New upstream version 9.5.2+dfsg4
authorFrancesco Ballarin <ballarin@debian.org>
Tue, 3 Mar 2026 15:04:38 +0000 (15:04 +0000)
committerFrancesco Ballarin <ballarin@debian.org>
Tue, 3 Mar 2026 15:04:38 +0000 (15:04 +0000)
73 files changed:
Web/Core/CMakeLists.txt [new file with mode: 0644]
Web/Core/Testing/CMakeLists.txt [new file with mode: 0644]
Web/Core/Testing/Cxx/CMakeLists.txt [new file with mode: 0644]
Web/Core/Testing/Cxx/TestDataEncoder.cxx [new file with mode: 0644]
Web/Core/Testing/Data/Baseline/TestDataEncoder.png.sha512 [new file with mode: 0644]
Web/Core/Testing/Data/Baseline/TestDataEncoder_1.png.sha512 [new file with mode: 0644]
Web/Core/Testing/Data/Baseline/TestRemoteInteractionAdapter.png.sha512 [new file with mode: 0644]
Web/Core/Testing/Python/CMakeLists.txt [new file with mode: 0644]
Web/Core/Testing/Python/TestDataEncoder.py [new file with mode: 0644]
Web/Core/Testing/Python/TestObjectIdMap.py [new file with mode: 0644]
Web/Core/Testing/Python/TestRemoteInteractionAdapter.py [new file with mode: 0644]
Web/Core/Testing/Python/TestWebApplicationMemory.py [new file with mode: 0644]
Web/Core/vtk.module [new file with mode: 0644]
Web/Core/vtkDataEncoder.cxx [new file with mode: 0644]
Web/Core/vtkDataEncoder.h [new file with mode: 0644]
Web/Core/vtkObjectIdMap.cxx [new file with mode: 0644]
Web/Core/vtkObjectIdMap.h [new file with mode: 0644]
Web/Core/vtkRemoteInteractionAdapter.cxx [new file with mode: 0644]
Web/Core/vtkRemoteInteractionAdapter.h [new file with mode: 0644]
Web/Core/vtkWebApplication.cxx [new file with mode: 0644]
Web/Core/vtkWebApplication.h [new file with mode: 0644]
Web/Core/vtkWebInteractionEvent.cxx [new file with mode: 0644]
Web/Core/vtkWebInteractionEvent.h [new file with mode: 0644]
Web/Core/vtkWebUtilities.cxx [new file with mode: 0644]
Web/Core/vtkWebUtilities.h [new file with mode: 0644]
Web/Python/CMakeLists.txt [new file with mode: 0644]
Web/Python/Testing/CMakeLists.txt [new file with mode: 0644]
Web/Python/Testing/Python/CMakeLists.txt [new file with mode: 0644]
Web/Python/Testing/Python/TestSerializeRenderWindow.py [new file with mode: 0644]
Web/Python/vtk.module [new file with mode: 0644]
Web/Python/vtkmodules/web/__init__.py [new file with mode: 0644]
Web/Python/vtkmodules/web/camera.py [new file with mode: 0644]
Web/Python/vtkmodules/web/dataset_builder.py [new file with mode: 0644]
Web/Python/vtkmodules/web/errors.py [new file with mode: 0644]
Web/Python/vtkmodules/web/protocols.py [new file with mode: 0644]
Web/Python/vtkmodules/web/query_data_model.py [new file with mode: 0644]
Web/Python/vtkmodules/web/render_window_serializer.py [new file with mode: 0644]
Web/Python/vtkmodules/web/testing.py [new file with mode: 0644]
Web/Python/vtkmodules/web/utils.py [new file with mode: 0644]
Web/Python/vtkmodules/web/venv.py [new file with mode: 0644]
Web/Python/vtkmodules/web/vtkjs_helper.py [new file with mode: 0644]
Web/Python/vtkmodules/web/wslink.py [new file with mode: 0644]
Web/WebAssembly/CMakeLists.txt [new file with mode: 0644]
Web/WebAssembly/Testing/CMakeLists.txt [new file with mode: 0644]
Web/WebAssembly/Testing/JavaScript/CMakeLists.txt [new file with mode: 0644]
Web/WebAssembly/Testing/JavaScript/testBindRenderWindow.mjs [new file with mode: 0644]
Web/WebAssembly/Testing/JavaScript/testBlobs.mjs [new file with mode: 0644]
Web/WebAssembly/Testing/JavaScript/testInitialize.mjs [new file with mode: 0644]
Web/WebAssembly/Testing/JavaScript/testInvoke.mjs [new file with mode: 0644]
Web/WebAssembly/Testing/JavaScript/testOSMesaRenderWindowPatch.mjs [new file with mode: 0644]
Web/WebAssembly/Testing/JavaScript/testSkipProperty.mjs [new file with mode: 0644]
Web/WebAssembly/Testing/JavaScript/testStates.mjs [new file with mode: 0644]
Web/WebAssembly/post.js [new file with mode: 0644]
Web/WebAssembly/vtk.module [new file with mode: 0644]
Web/WebAssembly/vtkWasmSceneManager.cxx [new file with mode: 0644]
Web/WebAssembly/vtkWasmSceneManager.h [new file with mode: 0644]
Web/WebAssembly/vtkWasmSceneManagerEmBinding.cxx [new file with mode: 0644]
Web/WebGLExporter/CMakeLists.txt [new file with mode: 0644]
Web/WebGLExporter/glMatrix.js [new file with mode: 0644]
Web/WebGLExporter/vtk.module [new file with mode: 0644]
Web/WebGLExporter/vtkPVWebGLExporter.cxx [new file with mode: 0644]
Web/WebGLExporter/vtkPVWebGLExporter.h [new file with mode: 0644]
Web/WebGLExporter/vtkWebGLDataSet.cxx [new file with mode: 0644]
Web/WebGLExporter/vtkWebGLDataSet.h [new file with mode: 0644]
Web/WebGLExporter/vtkWebGLExporter.cxx [new file with mode: 0644]
Web/WebGLExporter/vtkWebGLExporter.h [new file with mode: 0644]
Web/WebGLExporter/vtkWebGLObject.cxx [new file with mode: 0644]
Web/WebGLExporter/vtkWebGLObject.h [new file with mode: 0644]
Web/WebGLExporter/vtkWebGLPolyData.cxx [new file with mode: 0644]
Web/WebGLExporter/vtkWebGLPolyData.h [new file with mode: 0644]
Web/WebGLExporter/vtkWebGLWidget.cxx [new file with mode: 0644]
Web/WebGLExporter/vtkWebGLWidget.h [new file with mode: 0644]
Web/WebGLExporter/webglRenderer.js [new file with mode: 0644]

diff --git a/Web/Core/CMakeLists.txt b/Web/Core/CMakeLists.txt
new file mode 100644 (file)
index 0000000..5ce21cb
--- /dev/null
@@ -0,0 +1,11 @@
+set(classes
+  vtkDataEncoder
+  vtkObjectIdMap
+  vtkRemoteInteractionAdapter
+  vtkWebApplication
+  vtkWebInteractionEvent
+  vtkWebUtilities)
+
+vtk_module_add_module(VTK::WebCore
+  CLASSES ${classes})
+vtk_add_test_mangling(VTK::WebCore)
diff --git a/Web/Core/Testing/CMakeLists.txt b/Web/Core/Testing/CMakeLists.txt
new file mode 100644 (file)
index 0000000..c4378c8
--- /dev/null
@@ -0,0 +1,9 @@
+if (NOT vtk_testing_cxx_disabled)
+  add_subdirectory(Cxx)
+endif ()
+
+if (VTK_WRAP_PYTHON)
+  vtk_module_test_data(
+    Data/remote_events.json)
+  add_subdirectory(Python)
+endif ()
diff --git a/Web/Core/Testing/Cxx/CMakeLists.txt b/Web/Core/Testing/Cxx/CMakeLists.txt
new file mode 100644 (file)
index 0000000..1e60e45
--- /dev/null
@@ -0,0 +1,5 @@
+vtk_add_test_cxx(vtkWebCoreCxxTests tests
+  NO_VALID
+  TestDataEncoder.cxx)
+
+vtk_test_cxx_executable(vtkWebCoreCxxTests tests)
diff --git a/Web/Core/Testing/Cxx/TestDataEncoder.cxx b/Web/Core/Testing/Cxx/TestDataEncoder.cxx
new file mode 100644 (file)
index 0000000..9108323
--- /dev/null
@@ -0,0 +1,117 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+#include <vtkDataEncoder.h>
+#include <vtkImageCast.h>
+#include <vtkImageData.h>
+#include <vtkImageMandelbrotSource.h>
+#include <vtkLogger.h>
+#include <vtkNew.h>
+#include <vtkSmartPointer.h>
+
+#include <chrono>
+#include <thread>
+#include <vector>
+
+vtkSmartPointer<vtkImageData> GetData()
+{
+  vtkNew<vtkImageMandelbrotSource> source;
+  source->SetWholeExtent(0, 256, 0, 256, 0, 0);
+
+  vtkNew<vtkImageCast> caster;
+  caster->SetInputConnection(source->GetOutputPort());
+  caster->SetOutputScalarTypeToUnsignedChar();
+  caster->Update();
+  return caster->GetOutput();
+}
+
+bool TestCreate()
+{
+  vtkLogScopeFunction(INFO);
+  //--------------------------------------------------------------
+  // Create a bunch of instances and ensure it doesn't cause issues
+  // #18344
+  for (int cc = 0; cc < 100; cc++)
+  {
+    vtkNew<vtkDataEncoder> encoder;
+  }
+
+  std::vector<vtkSmartPointer<vtkDataEncoder>> encoders;
+  encoders.reserve(100);
+  for (int cc = 0; cc < 100; cc++)
+  {
+    encoders.push_back(vtk::TakeSmartPointer(vtkDataEncoder::New()));
+  }
+  return true;
+}
+
+bool TestFlush()
+{
+  vtkLogScopeFunction(INFO);
+  constexpr int KEY = 1020;
+
+  vtkNew<vtkDataEncoder> encoder;
+  encoder->SetMaxThreads(5);
+  encoder->Initialize();
+
+  // call flush without pushing any data.
+  encoder->Flush(KEY);
+
+  // push some data and then call flush.
+  for (int cc = 0; cc < 10; cc++)
+  {
+    encoder->Push(KEY, GetData(), 50);
+  }
+
+  encoder->Flush(KEY);
+
+  // call flush again.
+  encoder->Flush(KEY);
+
+  // push some data and then call flush.
+  for (int cc = 0; cc < 10; cc++)
+  {
+    encoder->Push(KEY, GetData(), 50);
+  }
+  std::this_thread::sleep_for(std::chrono::milliseconds(500));
+  encoder->Flush(KEY);
+
+  return true;
+}
+
+bool TestLatestOutput()
+{
+  vtkLogScopeFunction(INFO);
+  constexpr int KEY = 1020;
+
+  vtkNew<vtkDataEncoder> encoder;
+
+  vtkSmartPointer<vtkUnsignedCharArray> result;
+  if (encoder->GetLatestOutput(KEY, result))
+  {
+    vtkLogF(ERROR, "no output expected!");
+    return false;
+  }
+
+  // push some data and then call flush.
+  for (int cc = 0; cc < 10; cc++)
+  {
+    encoder->Push(KEY, GetData(), 50);
+  }
+
+  encoder->Flush(KEY);
+  if (!encoder->GetLatestOutput(KEY, result))
+  {
+    vtkLogF(ERROR, "latest output expected!");
+    return false;
+  }
+
+  return true;
+}
+
+int TestDataEncoder(int /*argc*/, char* /*argv*/[])
+{
+  TestCreate();
+  TestFlush();
+  TestLatestOutput();
+  return EXIT_SUCCESS;
+}
diff --git a/Web/Core/Testing/Data/Baseline/TestDataEncoder.png.sha512 b/Web/Core/Testing/Data/Baseline/TestDataEncoder.png.sha512
new file mode 100644 (file)
index 0000000..7e6932c
--- /dev/null
@@ -0,0 +1 @@
+19d5717b868631051688354258802885866eaf6fe3bbbdcd2e8410c2aa1b65ff4585d6943c751b54aa54d6b9c8fe50d97bcbf0c061c5fa63959a3d7e653abe0e
diff --git a/Web/Core/Testing/Data/Baseline/TestDataEncoder_1.png.sha512 b/Web/Core/Testing/Data/Baseline/TestDataEncoder_1.png.sha512
new file mode 100644 (file)
index 0000000..dfe6b6d
--- /dev/null
@@ -0,0 +1 @@
+9ef24c779e9c2176cfa88c195e3a8f1102bb3ad03593594e84c5c12f785100ac037e180ad7e56726874433bf2b72fe8e72569222de252ae45f4c224c57fd1fe3
diff --git a/Web/Core/Testing/Data/Baseline/TestRemoteInteractionAdapter.png.sha512 b/Web/Core/Testing/Data/Baseline/TestRemoteInteractionAdapter.png.sha512
new file mode 100644 (file)
index 0000000..002bcfb
--- /dev/null
@@ -0,0 +1 @@
+b05dd96550cd5a74ed49277cdf5efd635ef5a53b34152c4af90b0f4d007b3d2c46c3a450cf759cea36c2b84efdd3946853198b423bde8ac9ace8deaff446e258
diff --git a/Web/Core/Testing/Python/CMakeLists.txt b/Web/Core/Testing/Python/CMakeLists.txt
new file mode 100644 (file)
index 0000000..7a62f30
--- /dev/null
@@ -0,0 +1,10 @@
+vtk_add_test_python(
+  TestDataEncoder.py
+  TestRemoteInteractionAdapter.py
+  )
+
+vtk_add_test_python(
+  NO_DATA NO_VALID NO_OUTPUT
+  TestObjectIdMap.py
+  TestWebApplicationMemory.py
+  )
diff --git a/Web/Core/Testing/Python/TestDataEncoder.py b/Web/Core/Testing/Python/TestDataEncoder.py
new file mode 100644 (file)
index 0000000..778a794
--- /dev/null
@@ -0,0 +1,87 @@
+import sys
+from vtkmodules.vtkFiltersSources import vtkCylinderSource
+from vtkmodules.vtkIOCore import vtkBase64Utilities
+from vtkmodules.vtkRenderingCore import (
+    vtkActor,
+    vtkPolyDataMapper,
+    vtkRenderWindow,
+    vtkRenderer,
+    vtkWindowToImageFilter,
+)
+from vtkmodules.vtkTestingRendering import vtkTesting
+from vtkmodules.vtkWebCore import vtkDataEncoder
+import vtkmodules.vtkRenderingFreeType
+import vtkmodules.vtkRenderingOpenGL2
+import array
+from vtkmodules.test import Testing
+
+
+class TestDataEncoder(Testing.vtkTest):
+    def testEncodings(self):
+        # Render something
+        cylinder = vtkCylinderSource()
+        cylinder.SetResolution(8)
+
+        cylinderMapper = vtkPolyDataMapper()
+        cylinderMapper.SetInputConnection(cylinder.GetOutputPort())
+
+        cylinderActor = vtkActor()
+        cylinderActor.SetMapper(cylinderMapper)
+        cylinderActor.RotateX(30.0)
+        cylinderActor.RotateY(-45.0)
+
+        ren = vtkRenderer()
+        renWin = vtkRenderWindow()
+        renWin.AddRenderer(ren)
+        ren.AddActor(cylinderActor)
+        renWin.SetSize(200, 200)
+
+        ren.ResetCamera()
+        ren.GetActiveCamera().Zoom(1.5)
+        renWin.Render()
+
+        # Get a vtkImageData with the rendered output
+        w2if = vtkWindowToImageFilter()
+        w2if.SetInput(renWin)
+        w2if.SetShouldRerender(1)
+        w2if.SetReadFrontBuffer(0)
+        w2if.Update()
+        imgData = w2if.GetOutput()
+
+        # Use vtkDataEncoder to convert the image to PNG format and Base64 encode it
+        encoder = vtkDataEncoder()
+        base64String = encoder.EncodeAsBase64Png(imgData).encode('ascii')
+
+        # Now Base64 decode the string back to PNG image data bytes
+        inputArray = array.array('B', base64String)
+        outputBuffer = bytearray(len(inputArray))
+
+        try:
+            utils = vtkBase64Utilities()
+        except:
+            print('Unable to import required vtkBase64Utilities')
+            raise Exception("TestDataEncoder failed.")
+
+        actualLength = utils.DecodeSafely(inputArray, len(inputArray), outputBuffer, len(outputBuffer))
+        outputArray = bytearray(actualLength)
+        outputArray[:] = outputBuffer[0:actualLength]
+
+        # And write those bytes to the disk as an actual PNG image file
+        with open('TestDataEncoder.png', 'wb') as fd:
+            fd.write(outputArray)
+
+        # Create a vtkTesting object and specify a baseline image
+        rtTester = vtkTesting()
+        for arg in sys.argv[1:]:
+            rtTester.AddArgument(arg)
+        rtTester.AddArgument("-V")
+        rtTester.AddArgument("TestDataEncoder.png")
+
+        # Perform the image comparison test and print out the result.
+        result = rtTester.RegressionTest("TestDataEncoder.png", 0.05)
+
+        if result == 0:
+            raise Exception("TestDataEncoder failed.")
+
+if __name__ == "__main__":
+    Testing.main([(TestDataEncoder, 'test')])
diff --git a/Web/Core/Testing/Python/TestObjectIdMap.py b/Web/Core/Testing/Python/TestObjectIdMap.py
new file mode 100644 (file)
index 0000000..6c3d0ae
--- /dev/null
@@ -0,0 +1,40 @@
+from vtkmodules.vtkCommonCore import vtkObject
+from vtkmodules.vtkWebCore import vtkObjectIdMap
+from vtkmodules.test import Testing
+from vtkmodules.vtkWebCore import vtkWebApplication
+
+class TestObjectId(Testing.vtkTest):
+    def testObjId(self):
+        map = vtkObjectIdMap()
+        # Just make sure if we call it twice with None, the results match
+        objId1 = map.GetGlobalId(None)
+        objId1b = map.GetGlobalId(None)
+        print('Object ids for None: objId1 => ',objId1,', objId1b => ',objId1b)
+        self.assertTrue(objId1 == objId1b)
+
+        object2 = vtkObject()
+        addr2 = object2.__this__
+        addr2 = addr2[1:addr2.find('_', 1)]
+        addr2 = int(addr2, 16)
+
+        object3 = vtkObject()
+        addr3 = object3.__this__
+        addr3 = addr3[1:addr3.find('_', 1)]
+        addr3 = int(addr3, 16)
+
+        # insert the bigger address first
+        if (addr2 < addr3):
+            object2, object3 = object3, object2
+
+        objId2 = map.GetGlobalId(object2)
+        objId2b = map.GetGlobalId(object2)
+        print('Object ids for object2: objId2 => ',objId2,', objId2b => ',objId2b)
+        self.assertTrue(objId2 == objId2b)
+
+        objId3 = map.GetGlobalId(object3)
+        objId3b = map.GetGlobalId(object3)
+        print('Object ids for object3: objId3 => ',objId3,', objId3b => ',objId3b)
+        self.assertTrue(objId3 == objId3b)
+
+if __name__ == "__main__":
+    Testing.main([(TestObjectId, 'test')])
diff --git a/Web/Core/Testing/Python/TestRemoteInteractionAdapter.py b/Web/Core/Testing/Python/TestRemoteInteractionAdapter.py
new file mode 100644 (file)
index 0000000..f05c568
--- /dev/null
@@ -0,0 +1,99 @@
+# SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+# SPDX-License-Identifier: BSD-3-Clause
+
+""" Apply a series of events produced by vtk-js RenderWindowInteractor to a
+vtkRenderwindow via the vtkRemoteInteractionAdapter class. The final image is
+the expected scene after all interactions have been applied.
+"""
+
+from vtkmodules.vtkRenderingCore import (
+    vtkActor,
+    vtkPolyDataMapper,
+    vtkRenderer,
+    vtkRenderWindow,
+    vtkRenderWindowInteractor,
+)
+from vtkmodules.vtkWebCore import vtkRemoteInteractionAdapter
+from vtkmodules.vtkFiltersSources import vtkConeSource
+
+from vtkmodules.test import Testing
+import os
+import json
+
+# Required for rendering initialization,
+import vtkmodules.vtkRenderingOpenGL2  # noqa
+
+# Required for interactor initialization
+from vtkmodules.vtkInteractionStyle import vtkInteractorStyleSwitch  # noqa
+
+
+# The scene to test. In some platforms reusing the window & renderer across
+# the two test cases causes segfault. We start all tests from a clean state by
+# creating the scene from scratch each time.
+class Scene:
+    def __init__(self):
+        self.dataFile = os.path.join(
+            Testing.VTK_DATA_ROOT, "Data", "remote_events.json"
+        )
+        self.imageFile = "TestRemoteInteractionAdapter.png"
+        self.adapter = vtkRemoteInteractionAdapter()
+
+        print("dataFile: {}".format(self.dataFile))
+        if not os.path.isfile(self.dataFile):
+            raise RuntimeError("Datafile is missing")
+
+        self.renderer = vtkRenderer()
+        self.renderWindow = vtkRenderWindow()
+        self.renderWindow.AddRenderer(self.renderer)
+        self.renderWindow.SetSize(300, 300)
+
+        self.renderWindowInteractor = vtkRenderWindowInteractor()
+        self.renderWindowInteractor.SetRenderWindow(self.renderWindow)
+        self.renderWindowInteractor.GetInteractorStyle().SetCurrentStyleToTrackballCamera()
+
+        self.cone_source = vtkConeSource()
+        self.mapper = vtkPolyDataMapper()
+        self.mapper.SetInputConnection(self.cone_source.GetOutputPort())
+        self.actor = vtkActor()
+        self.actor.SetMapper(self.mapper)
+
+        self.renderer.AddActor(self.actor)
+        self.renderer.ResetCamera()
+        self.renderWindowInteractor.Initialize()
+
+
+class TestRemoteInteractorAdapter(Testing.vtkTest):
+    def test0(self):
+        """Use class methods API for ProcessEvent"""
+        scene = Scene()
+
+        adapter = vtkRemoteInteractionAdapter()
+        adapter.SetInteractor(scene.renderWindowInteractor)
+
+        with open(scene.dataFile, "r") as f:
+            data = json.load(f)
+            for event in data["events"]:
+                event_str = json.dumps(event)
+                status = adapter.ProcessEvent(event_str)
+                assert status, f"Failed to process event\n {event_str}"
+                scene.renderWindowInteractor.Render()
+        self.assertImageMatch(scene.renderWindow, scene.imageFile)
+
+    def test1(self):
+        """Use static method API for ProcessEvent"""
+        scene = Scene()
+
+        with open(scene.dataFile, "r") as f:
+            data = json.load(f)
+            for event in data["events"]:
+                event_str = json.dumps(event)
+                status = vtkRemoteInteractionAdapter.ProcessEvent(
+                    scene.renderWindowInteractor, event_str
+                )
+                assert status, f"Failed to process event\n {event_str}"
+                scene.renderWindowInteractor.Render()
+        self.assertImageMatch(scene.renderWindow, scene.imageFile)
+
+
+if __name__ == "__main__":
+    Testing.main([(TestRemoteInteractorAdapter, "test")])
diff --git a/Web/Core/Testing/Python/TestWebApplicationMemory.py b/Web/Core/Testing/Python/TestWebApplicationMemory.py
new file mode 100644 (file)
index 0000000..cfac38f
--- /dev/null
@@ -0,0 +1,42 @@
+from vtkmodules.vtkFiltersSources import vtkCylinderSource
+from vtkmodules.vtkRenderingCore import (
+    vtkActor,
+    vtkPolyDataMapper,
+    vtkRenderWindow,
+    vtkRenderer,
+)
+from vtkmodules.vtkWebCore import vtkWebApplication
+import vtkmodules.vtkRenderingFreeType
+import vtkmodules.vtkRenderingOpenGL2
+from vtkmodules.test import Testing
+from vtkmodules.vtkWebCore import vtkWebApplication
+
+class TestWebApplicationMemory(Testing.vtkTest):
+    def testWebApplicationMemory(self):
+        cylinder = vtkCylinderSource()
+        cylinder.SetResolution(8)
+
+        cylinderMapper = vtkPolyDataMapper()
+        cylinderMapper.SetInputConnection(cylinder.GetOutputPort())
+
+        cylinderActor = vtkActor()
+        cylinderActor.SetMapper(cylinderMapper)
+        cylinderActor.RotateX(30.0)
+        cylinderActor.RotateY(-45.0)
+
+        ren = vtkRenderer()
+        renWin = vtkRenderWindow()
+        renWin.AddRenderer(ren)
+        ren.AddActor(cylinderActor)
+        renWin.SetSize(200, 200)
+
+        ren.ResetCamera()
+        ren.GetActiveCamera().Zoom(1.5)
+        renWin.Render()
+
+        webApp = vtkWebApplication()
+        # no memory leaks should be reported when compiling with VTK_DEBUG_LEAKS
+        webApp.StillRender(renWin)
+
+if __name__ == "__main__":
+    Testing.main([(TestWebApplicationMemory, 'test')])
diff --git a/Web/Core/vtk.module b/Web/Core/vtk.module
new file mode 100644 (file)
index 0000000..f6a9e09
--- /dev/null
@@ -0,0 +1,31 @@
+NAME
+  VTK::WebCore
+LIBRARY_NAME
+  vtkWebCore
+GROUPS
+  Web
+SPDX_LICENSE_IDENTIFIER
+  BSD-3-Clause
+SPDX_COPYRIGHT_TEXT
+  Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+DEPENDS
+  VTK::CommonCore
+PRIVATE_DEPENDS
+  VTK::CommonDataModel
+  VTK::CommonSystem
+  VTK::FiltersGeneral
+  VTK::FiltersGeometry
+  VTK::IOCore
+  VTK::IOImage
+  VTK::ParallelCore
+  VTK::Python
+  VTK::RenderingCore
+  VTK::WebGLExporter
+  VTK::vtksys
+  VTK::nlohmannjson
+TEST_LABELS
+  VTK::Web
+TEST_DEPENDS
+  VTK::ImagingCore
+  VTK::ImagingSources
+  VTK::TestingCore
diff --git a/Web/Core/vtkDataEncoder.cxx b/Web/Core/vtkDataEncoder.cxx
new file mode 100644 (file)
index 0000000..e93ca58
--- /dev/null
@@ -0,0 +1,334 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+#include "vtkDataEncoder.h"
+
+#include "vtkBase64Utilities.h"
+#include "vtkCommand.h"
+#include "vtkImageData.h"
+#include "vtkJPEGWriter.h"
+#include "vtkLogger.h"
+#include "vtkNew.h"
+#include "vtkObjectFactory.h"
+#include "vtkPNGWriter.h"
+#include "vtkSmartPointer.h"
+#include "vtkUnsignedCharArray.h"
+
+#include <cassert>
+#include <cmath>
+#include <condition_variable>
+#include <map>
+#include <mutex>
+#include <queue>
+#include <thread>
+#include <vector>
+
+#include <vtksys/SystemTools.hxx>
+
+#define MAX_NUMBER_OF_THREADS_IN_POOL 32
+
+namespace detail
+{
+VTK_ABI_NAMESPACE_BEGIN
+
+struct vtkWork
+{
+  vtkSmartPointer<vtkImageData> Image;
+  int Quality = 0;
+  int Encoding = 0;
+  vtkTypeUInt64 TimeStamp = 0;
+  vtkTypeUInt32 Key = 0;
+
+  vtkWork() = default;
+  vtkWork(vtkTypeUInt32 key, vtkImageData* image, int quality, int encoding)
+    : Image(image)
+    , Quality(quality)
+    , Encoding(encoding)
+    , TimeStamp(0)
+    , Key(key)
+  {
+  }
+  vtkWork(const vtkWork&) = default;
+  vtkWork& operator=(const vtkWork&) = default;
+};
+
+class vtkWorkQueue
+{
+  mutable std::mutex ResultsMutex;
+  std::map<vtkTypeUInt32, std::pair<vtkTypeUInt64, vtkSmartPointer<vtkUnsignedCharArray>>> Results;
+  std::condition_variable ResultsCondition;
+
+  std::map<vtkTypeUInt32, std::atomic<vtkTypeUInt32>> LastTimeStamp;
+
+  std::mutex QueueMutex;
+  std::queue<vtkWork> Queue;
+  std::condition_variable QueueCondition;
+
+  std::vector<std::thread> ThreadPool;
+  std::atomic<bool> Terminate;
+
+  static void DoWork(int threadIndex, vtkWorkQueue* self)
+  {
+    vtkLogger::SetThreadName("Worker " + std::to_string(threadIndex));
+    vtkLogF(TRACE, "starting worker thread");
+    vtkNew<vtkJPEGWriter> writer;
+    writer->WriteToMemoryOn();
+    while (!self->Terminate)
+    {
+      vtkWork work;
+      {
+        std::unique_lock<std::mutex> lock(self->QueueMutex);
+        bool break_loop = false;
+        do
+        {
+          self->QueueCondition.wait_for(lock, std::chrono::seconds(1),
+            [self]() { return !self->Queue.empty() || self->Terminate; });
+          if (self->Terminate)
+          {
+            break_loop = true;
+            break;
+          }
+        } while (self->Queue.empty());
+        if (break_loop)
+        {
+          break;
+        }
+        work = self->Queue.front();
+        self->Queue.pop();
+      }
+
+      writer->SetInputData(work.Image);
+      writer->SetQuality(work.Quality);
+      writer->Write();
+
+      auto result = vtkSmartPointer<vtkUnsignedCharArray>::New();
+      if (work.Encoding)
+      {
+        vtkUnsignedCharArray* data = writer->GetResult();
+        result->SetNumberOfComponents(1);
+        result->SetNumberOfTuples(std::ceil(1.5 * data->GetNumberOfTuples()));
+        unsigned long size = vtkBase64Utilities::Encode(
+          data->GetPointer(0), data->GetNumberOfTuples(), result->GetPointer(0), /*mark_end=*/0);
+        result->SetNumberOfTuples(static_cast<vtkIdType>(size) + 1);
+        result->SetValue(size, 0);
+      }
+      else
+      {
+        // We must do a deep copy here as the writer reuse that array
+        // and will change its values concurrently during its next job...
+        result->DeepCopy(writer->GetResult());
+      }
+      writer->SetInputData(nullptr);
+
+      {
+        std::unique_lock<std::mutex> lock(self->ResultsMutex);
+        auto& pair = self->Results[work.Key];
+        if (pair.first < work.TimeStamp)
+        {
+          pair = std::make_pair(work.TimeStamp, result);
+          lock.unlock();
+          self->ResultsCondition.notify_all();
+        }
+      }
+    }
+
+    vtkLogF(TRACE, "exiting worker thread");
+  }
+
+public:
+  vtkWorkQueue(int numThreads)
+    : Terminate(false)
+  {
+    assert(numThreads >= 0);
+    for (int cc = 0; cc < numThreads; ++cc)
+    {
+      this->ThreadPool.emplace_back(&vtkWorkQueue::DoWork, cc, this);
+    }
+  }
+  ~vtkWorkQueue()
+  {
+    this->Terminate = true;
+    this->QueueCondition.notify_all();
+    for (auto& thread : this->ThreadPool)
+    {
+      thread.join();
+    }
+  }
+
+  bool IsValid() const { return !this->ThreadPool.empty(); }
+
+  void PushBack(vtkWork&& work)
+  {
+    if (!this->IsValid())
+    {
+      vtkLogF(ERROR, "Queue is invalid! Can't push work!");
+      return;
+    }
+
+    auto key = work.Key;
+    work.TimeStamp = ++this->LastTimeStamp[key];
+    {
+      std::unique_lock<std::mutex> lock(this->QueueMutex);
+      this->Queue.emplace(std::move(work));
+    }
+    this->QueueCondition.notify_one();
+  }
+
+  bool GetResult(vtkTypeUInt32 key, vtkSmartPointer<vtkUnsignedCharArray>& data) const
+  {
+    std::unique_lock<std::mutex> lock(this->ResultsMutex);
+    auto iter = this->Results.find(key);
+    if (iter == this->Results.end())
+    {
+      return false;
+    }
+
+    const auto& resultsPair = iter->second;
+    data = resultsPair.second;
+    // return true if this is the latest result for this key.
+    return (resultsPair.first == this->LastTimeStamp.at(key));
+  }
+
+  void Flush(vtkTypeUInt32 key)
+  {
+    auto tsIter = this->LastTimeStamp.find(key);
+    if (tsIter == this->LastTimeStamp.end())
+    {
+      return;
+    }
+    const auto& ts = tsIter->second;
+    std::unique_lock<std::mutex> lock(this->ResultsMutex);
+    this->ResultsCondition.wait(lock,
+      [this, &ts, &key]()
+      {
+        try
+        {
+          return ts == this->Results[key].first;
+        }
+        catch (std::out_of_range&)
+        {
+          // result not available yet; keep waiting;
+          return false;
+        }
+      });
+  }
+};
+VTK_ABI_NAMESPACE_END
+} // namespace detail
+
+VTK_ABI_NAMESPACE_BEGIN
+//****************************************************************************
+class vtkDataEncoder::vtkInternals
+{
+public:
+  detail::vtkWorkQueue Queue;
+  vtkNew<vtkUnsignedCharArray> LastBase64Image;
+
+  vtkInternals(int numThreads)
+    : Queue(numThreads)
+  {
+  }
+
+  // Once an imagedata has been written to memory as a jpg or png, this
+  // convenience function can encode that image as a Base64 string.
+  const char* GetBase64EncodedImage(vtkUnsignedCharArray* encodedInputImage)
+  {
+    this->LastBase64Image->SetNumberOfComponents(1);
+    this->LastBase64Image->SetNumberOfTuples(
+      std::ceil(1.5 * encodedInputImage->GetNumberOfTuples()));
+    unsigned long size = vtkBase64Utilities::Encode(encodedInputImage->GetPointer(0),
+      encodedInputImage->GetNumberOfTuples(), this->LastBase64Image->GetPointer(0), /*mark_end=*/0);
+
+    this->LastBase64Image->SetNumberOfTuples(static_cast<vtkIdType>(size) + 1);
+    this->LastBase64Image->SetValue(size, 0);
+
+    return reinterpret_cast<char*>(this->LastBase64Image->GetPointer(0));
+  }
+};
+
+vtkStandardNewMacro(vtkDataEncoder);
+//------------------------------------------------------------------------------
+vtkDataEncoder::vtkDataEncoder()
+  : MaxThreads(3)
+  , Internals(new vtkInternals(this->MaxThreads))
+{
+}
+
+//------------------------------------------------------------------------------
+vtkDataEncoder::~vtkDataEncoder() = default;
+
+//------------------------------------------------------------------------------
+void vtkDataEncoder::SetMaxThreads(vtkTypeUInt32 maxThreads)
+{
+  if (maxThreads < MAX_NUMBER_OF_THREADS_IN_POOL && maxThreads > 0)
+  {
+    this->MaxThreads = maxThreads;
+  }
+}
+
+//------------------------------------------------------------------------------
+void vtkDataEncoder::Initialize()
+{
+  this->Internals.reset(new vtkDataEncoder::vtkInternals(this->MaxThreads));
+}
+
+//------------------------------------------------------------------------------
+void vtkDataEncoder::Push(vtkTypeUInt32 key, vtkImageData* data, int quality, int encoding)
+{
+  auto& internals = (*this->Internals);
+  internals.Queue.PushBack(detail::vtkWork(key, data, quality, encoding));
+}
+
+//------------------------------------------------------------------------------
+bool vtkDataEncoder::GetLatestOutput(vtkTypeUInt32 key, vtkSmartPointer<vtkUnsignedCharArray>& data)
+{
+  auto& internals = (*this->Internals);
+  return internals.Queue.GetResult(key, data);
+}
+
+//------------------------------------------------------------------------------
+const char* vtkDataEncoder::EncodeAsBase64Png(vtkImageData* img, int compressionLevel)
+{
+  // Perform in-memory write of image as png
+  vtkNew<vtkPNGWriter> writer;
+  writer->WriteToMemoryOn();
+  writer->SetInputData(img);
+  writer->SetCompressionLevel(compressionLevel);
+  writer->Write();
+
+  // Return Base64-encoded string
+  return this->Internals->GetBase64EncodedImage(writer->GetResult());
+}
+
+//------------------------------------------------------------------------------
+const char* vtkDataEncoder::EncodeAsBase64Jpg(vtkImageData* img, int quality)
+{
+  // Perform in-memory write of image as jpg
+  vtkNew<vtkJPEGWriter> writer;
+  writer->WriteToMemoryOn();
+  writer->SetInputData(img);
+  writer->SetQuality(quality);
+  writer->Write();
+
+  // Return Base64-encoded string
+  return this->Internals->GetBase64EncodedImage(writer->GetResult());
+}
+
+//------------------------------------------------------------------------------
+void vtkDataEncoder::Flush(vtkTypeUInt32 key)
+{
+  auto& internals = (*this->Internals);
+  internals.Queue.Flush(key);
+}
+
+//------------------------------------------------------------------------------
+void vtkDataEncoder::PrintSelf(ostream& os, vtkIndent indent)
+{
+  this->Superclass::PrintSelf(os, indent);
+}
+
+//------------------------------------------------------------------------------
+void vtkDataEncoder::Finalize()
+{
+  this->Internals.reset(new vtkDataEncoder::vtkInternals(0));
+}
+VTK_ABI_NAMESPACE_END
diff --git a/Web/Core/vtkDataEncoder.h b/Web/Core/vtkDataEncoder.h
new file mode 100644 (file)
index 0000000..2fbc7ea
--- /dev/null
@@ -0,0 +1,110 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+/**
+ * @class   vtkDataEncoder
+ * @brief   class used to compress/encode images using threads.
+ *
+ * vtkDataEncoder is used to compress and encode images using threads.
+ * Multiple images can be pushed into the encoder for compression and encoding.
+ * We use a vtkTypeUInt32 as the key to identify different image pipes. The
+ * images in each pipe will be processed in parallel threads. The latest
+ * compressed and encoded image can be accessed using GetLatestOutput().
+ *
+ * vtkDataEncoder uses a thread-pool to do the compression and encoding in
+ * parallel.  Note that images may not come out of the vtkDataEncoder in the
+ * same order as they are pushed in, if an image pushed in at N-th location
+ * takes longer to compress and encode than that pushed in at N+1-th location or
+ * if it was pushed in before the N-th location was even taken up for encoding
+ * by the a thread in the thread pool.
+ */
+
+#ifndef vtkDataEncoder_h
+#define vtkDataEncoder_h
+
+#include "vtkObject.h"
+#include "vtkSmartPointer.h"  // needed for vtkSmartPointer
+#include "vtkWebCoreModule.h" // needed for exports
+#include <memory>             // for std::unique_ptr
+
+VTK_ABI_NAMESPACE_BEGIN
+class vtkUnsignedCharArray;
+class vtkImageData;
+
+class VTKWEBCORE_EXPORT vtkDataEncoder : public vtkObject
+{
+public:
+  static vtkDataEncoder* New();
+  vtkTypeMacro(vtkDataEncoder, vtkObject);
+  void PrintSelf(ostream& os, vtkIndent indent) override;
+
+  ///@{
+  /**
+   * Define the number of worker threads to use. Default is 3.
+   * Initialize() needs to be called after changing the thread count.
+   */
+  void SetMaxThreads(vtkTypeUInt32);
+  vtkGetMacro(MaxThreads, vtkTypeUInt32);
+  ///@}
+
+  /**
+   * Re-initializes the encoder. This will abort any on going encoding threads
+   * and clear internal data-structures.
+   */
+  void Initialize();
+
+  /**
+   * Push an image into the encoder. The data is considered unchanging and thus
+   * should not be modified once pushed. Reference count changes are now thread safe
+   * and hence callers should ensure they release the reference held, if
+   * appropriate.
+   */
+  void Push(vtkTypeUInt32 key, vtkImageData* data, int quality, int encoding = 1);
+
+  /**
+   * Get access to the most-recent fully encoded result corresponding to the
+   * given key, if any. This methods returns true if the \c data obtained is the
+   * result from the most recent Push() for the key, if any. If this method
+   * returns false, it means that there's some image either being processed on
+   * pending processing.
+   */
+  bool GetLatestOutput(vtkTypeUInt32 key, vtkSmartPointer<vtkUnsignedCharArray>& data);
+
+  /**
+   * Flushes the encoding pipe and blocks till the most recently pushed image
+   * for the particular key has been processed. This call will block. Once this
+   * method returns, caller can use GetLatestOutput(key) to access the processed
+   * output.
+   */
+  void Flush(vtkTypeUInt32 key);
+
+  /**
+   * Take an image data and synchronously convert it to a base-64 encoded png.
+   */
+  const char* EncodeAsBase64Png(vtkImageData* img, int compressionLevel = 5);
+
+  /**
+   * Take an image data and synchronously convert it to a base-64 encoded jpg.
+   */
+  const char* EncodeAsBase64Jpg(vtkImageData* img, int quality = 50);
+
+  /**
+   * This method will wait for any running thread to terminate.
+   */
+  void Finalize();
+
+protected:
+  vtkDataEncoder();
+  ~vtkDataEncoder() override;
+
+  vtkTypeUInt32 MaxThreads;
+
+private:
+  vtkDataEncoder(const vtkDataEncoder&) = delete;
+  void operator=(const vtkDataEncoder&) = delete;
+
+  class vtkInternals;
+  std::unique_ptr<vtkInternals> Internals;
+};
+
+VTK_ABI_NAMESPACE_END
+#endif
diff --git a/Web/Core/vtkObjectIdMap.cxx b/Web/Core/vtkObjectIdMap.cxx
new file mode 100644 (file)
index 0000000..bd676ce
--- /dev/null
@@ -0,0 +1,123 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+#include "vtkObjectIdMap.h"
+
+#include "vtkObjectFactory.h"
+#include "vtkSmartPointer.h"
+#include "vtkWeakPointer.h"
+
+#include <map>
+#include <set>
+#include <string>
+
+VTK_ABI_NAMESPACE_BEGIN
+struct vtkObjectIdMap::vtkInternals
+{
+  std::map<vtkTypeUInt32, vtkSmartPointer<vtkObject>> Object;
+  std::map<vtkSmartPointer<vtkObject>, vtkTypeUInt32> GlobalId;
+  std::map<std::string, vtkWeakPointer<vtkObject>> ActiveObjects;
+  vtkTypeUInt32 NextAvailableId;
+
+  vtkInternals()
+    : NextAvailableId(1)
+  {
+  }
+};
+
+vtkStandardNewMacro(vtkObjectIdMap);
+//------------------------------------------------------------------------------
+vtkObjectIdMap::vtkObjectIdMap()
+  : Internals(new vtkInternals())
+{
+}
+
+//------------------------------------------------------------------------------
+vtkObjectIdMap::~vtkObjectIdMap()
+{
+  delete this->Internals;
+  this->Internals = nullptr;
+}
+
+//------------------------------------------------------------------------------
+void vtkObjectIdMap::PrintSelf(ostream& os, vtkIndent indent)
+{
+  this->Superclass::PrintSelf(os, indent);
+}
+
+//------------------------------------------------------------------------------
+vtkTypeUInt32 vtkObjectIdMap::GetGlobalId(vtkObject* obj)
+{
+  if (obj == nullptr)
+  {
+    return 0;
+  }
+
+  auto iter = this->Internals->GlobalId.find(obj);
+  if (iter == this->Internals->GlobalId.end())
+  {
+    vtkTypeUInt32 globalId = this->Internals->NextAvailableId++;
+    this->Internals->GlobalId[obj] = globalId;
+    this->Internals->Object[globalId] = obj;
+    return globalId;
+  }
+  return iter->second;
+}
+
+//------------------------------------------------------------------------------
+vtkObject* vtkObjectIdMap::GetVTKObject(vtkTypeUInt32 globalId)
+{
+  auto iter = this->Internals->Object.find(globalId);
+  if (iter == this->Internals->Object.end())
+  {
+    return nullptr;
+  }
+  return iter->second;
+}
+
+//------------------------------------------------------------------------------
+vtkTypeUInt32 vtkObjectIdMap::SetActiveObject(const char* objectType, vtkObject* obj)
+{
+  if (objectType)
+  {
+    this->Internals->ActiveObjects[objectType] = obj;
+    return this->GetGlobalId(obj);
+  }
+  return 0;
+}
+
+//------------------------------------------------------------------------------
+vtkObject* vtkObjectIdMap::GetActiveObject(const char* objectType)
+{
+  if (objectType)
+  {
+    return this->Internals->ActiveObjects[objectType];
+  }
+  return nullptr;
+}
+
+//------------------------------------------------------------------------------
+bool vtkObjectIdMap::FreeObject(vtkObject* obj)
+{
+  auto iter = this->Internals->GlobalId.find(obj);
+  auto found = iter != this->Internals->GlobalId.end();
+  if (found)
+  {
+    this->Internals->Object.erase(iter->second);
+    this->Internals->GlobalId.erase(iter);
+  }
+  return found;
+}
+
+//------------------------------------------------------------------------------
+bool vtkObjectIdMap::FreeObjectById(vtkTypeUInt32 id)
+{
+  auto iter = this->Internals->Object.find(id);
+  auto found = iter != this->Internals->Object.end();
+  if (found)
+  {
+    this->Internals->GlobalId.erase(iter->second);
+    this->Internals->Object.erase(iter);
+  }
+  return found;
+}
+VTK_ABI_NAMESPACE_END
diff --git a/Web/Core/vtkObjectIdMap.h b/Web/Core/vtkObjectIdMap.h
new file mode 100644 (file)
index 0000000..2f78521
--- /dev/null
@@ -0,0 +1,74 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+/**
+ * @class   vtkObjectIdMap
+ * @brief   class used to assign Id to any VTK object and be able
+ * to retrieve it base on its id.
+ */
+
+#ifndef vtkObjectIdMap_h
+#define vtkObjectIdMap_h
+
+#include "vtkObject.h"
+#include "vtkWebCoreModule.h" // needed for exports
+
+VTK_ABI_NAMESPACE_BEGIN
+class VTKWEBCORE_EXPORT vtkObjectIdMap : public vtkObject
+{
+public:
+  static vtkObjectIdMap* New();
+  vtkTypeMacro(vtkObjectIdMap, vtkObject);
+  void PrintSelf(ostream& os, vtkIndent indent) override;
+
+  /**
+   * Retrieve a unique identifier for the given object or generate a new one
+   * if its global id was never requested.
+   */
+  vtkTypeUInt32 GetGlobalId(vtkObject* obj);
+
+  /**
+   * Retrieve a vtkObject based on its global id. If not found return nullptr
+   */
+  vtkObject* GetVTKObject(vtkTypeUInt32 globalId);
+
+  /**
+   * Assign an active key (string) to an existing object.
+   * This is usually used to provide another type of access to specific
+   * vtkObject that we want to retrieve easily using a string.
+   * Return the global Id of the given registered object
+   */
+  vtkTypeUInt32 SetActiveObject(const char* objectType, vtkObject* obj);
+
+  /**
+   * Retrieve a previously stored object based on a name
+   */
+  vtkObject* GetActiveObject(const char* objectType);
+
+  /**
+   * Given an object, remove any internal reference count due to
+   * internal Id/Object mapping.
+   * Returns true if the item existed in the map and was deleted.
+   */
+  bool FreeObject(vtkObject* obj);
+
+  /**
+   * Given an id, remove any internal reference count due to
+   * internal Id/Object mapping.
+   * Returns true if the id existed in the map and was deleted.
+   */
+  bool FreeObjectById(vtkTypeUInt32 id);
+
+protected:
+  vtkObjectIdMap();
+  ~vtkObjectIdMap() override;
+
+private:
+  vtkObjectIdMap(const vtkObjectIdMap&) = delete;
+  void operator=(const vtkObjectIdMap&) = delete;
+
+  struct vtkInternals;
+  vtkInternals* Internals;
+};
+
+VTK_ABI_NAMESPACE_END
+#endif
diff --git a/Web/Core/vtkRemoteInteractionAdapter.cxx b/Web/Core/vtkRemoteInteractionAdapter.cxx
new file mode 100644 (file)
index 0000000..a4167cd
--- /dev/null
@@ -0,0 +1,291 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include "vtkRemoteInteractionAdapter.h"
+#include "vtkCommand.h"
+#include "vtkLogger.h"
+#include "vtkObjectFactory.h"
+#include "vtkRenderWindow.h"
+#include "vtkRenderWindowInteractor.h"
+
+#include <vtk_nlohmannjson.h>
+#include VTK_NLOHMANN_JSON(json.hpp)
+
+#include <unordered_map>
+
+VTK_ABI_NAMESPACE_BEGIN
+
+enum
+{
+  WheelEvent = vtkCommand::UserEvent + 3000,
+};
+
+// map vtk-js event codes to vtkCommand events , for the ones that I didn't found a clear
+// correspondence I used vtkCommand::NoEvent and left them unhandled. Taken from
+// https://github.com/Kitware/vtk-js/blob/master/Sources/Rendering/Core/RenderWindowInteractor/index.js
+
+using enum_type = int;
+// clang-format off
+const std::unordered_map< std::string, enum_type > EVENT_MAP {
+  {"StartAnimation"             ,vtkCommand::NoEvent},
+  {"Animation"                  ,vtkCommand::NoEvent},
+  {"EndAnimation"               ,vtkCommand::NoEvent},
+  {"PointerEnter"               ,vtkCommand::EnterEvent},
+  {"PointerLeave"               ,vtkCommand::LeaveEvent},
+  {"MouseEnter"                 ,vtkCommand::EnterEvent},
+  {"MouseLeave"                 ,vtkCommand::LeaveEvent},
+  {"StartMouseMove"             ,vtkCommand::NoEvent},
+  {"MouseMove"                  ,vtkCommand::MouseMoveEvent},
+  {"EndMouseMove"               ,vtkCommand::NoEvent},
+  {"LeftButtonPress"            ,vtkCommand::LeftButtonPressEvent},
+  {"LeftButtonRelease"          ,vtkCommand::LeftButtonReleaseEvent},
+  {"MiddleButtonPress"          ,vtkCommand::MiddleButtonPressEvent},
+  {"MiddleButtonRelease"        ,vtkCommand::MiddleButtonReleaseEvent},
+  {"RightButtonPress"           ,vtkCommand::RightButtonPressEvent},
+  {"RightButtonRelease"         ,vtkCommand::RightButtonReleaseEvent},
+  {"KeyPress"                   ,vtkCommand::KeyPressEvent},
+  {"KeyDown"                    ,vtkCommand::KeyPressEvent},
+  {"KeyUp"                      ,vtkCommand::KeyReleaseEvent},
+  {"StartMouseWheel"            ,vtkCommand::NoEvent},
+  {"MouseWheel"                 ,WheelEvent},
+  {"EndMouseWheel"              ,vtkCommand::NoEvent},
+  {"StartPinch"                 ,vtkCommand::StartPinchEvent},
+  {"Pinch"                      ,vtkCommand::PinchEvent},
+  {"EndPinch"                   ,vtkCommand::EndPinchEvent},
+  {"StartPan"                   ,vtkCommand::StartPanEvent},
+  {"Pan"                        ,vtkCommand::PanEvent},
+  {"EndPan"                     ,vtkCommand::EndPanEvent},
+  {"StartRotate"                ,vtkCommand::StartRotateEvent},
+  {"Rotate"                     ,vtkCommand::RotateEvent},
+  {"EndRotate"                  ,vtkCommand::RenderEvent},
+  {"Button3D"                   ,vtkCommand::NoEvent},
+  {"Move3D"                     ,vtkCommand::NoEvent},
+  {"StartPointerLock"           ,vtkCommand::NoEvent},
+  {"EndPointerLock"             ,vtkCommand::NoEvent},
+  {"StartInteraction"           ,vtkCommand::NoEvent},
+  {"Interaction"                ,vtkCommand::NoEvent},
+  {"EndInteraction"             ,vtkCommand::NoEvent},
+  {"AnimationFrameRateUpdate"   ,vtkCommand::NoEvent}
+};
+// clang-format on
+
+vtkSetObjectImplementationMacro(vtkRemoteInteractionAdapter, Interactor, vtkRenderWindowInteractor);
+//----------------------------------------------------------------------------
+vtkStandardNewMacro(vtkRemoteInteractionAdapter);
+
+//----------------------------------------------------------------------------
+vtkRemoteInteractionAdapter::vtkRemoteInteractionAdapter() = default;
+
+//----------------------------------------------------------------------------
+vtkRemoteInteractionAdapter::~vtkRemoteInteractionAdapter()
+{
+  this->SetInteractor(nullptr);
+}
+
+//----------------------------------------------------------------------------
+void vtkRemoteInteractionAdapter::PrintSelf(ostream& os, vtkIndent indent)
+{
+  this->Superclass::PrintSelf(os, indent);
+}
+
+//----------------------------------------------------------------------------
+// based on QVTKInteractorAdapter::ProcessEvent(QEvent* e, vtkRenderWindowInteractor* iren)
+bool vtkRemoteInteractionAdapter::ProcessEvent(vtkRenderWindowInteractor* iren,
+  const std::string& event_str, double devicePixelRatio, double devicePixelRatioTolerance)
+{
+
+  if (!iren)
+  {
+    vtkLogF(ERROR, "Null interactor passed");
+    return false;
+  }
+  // the following events only happen if the interactor is enabled
+  if (!iren->GetEnabled())
+  {
+    return false;
+  }
+
+  try
+  {
+    nlohmann::json event = nlohmann::json::parse(event_str);
+    const std::string& type = event.at("type");
+    vtkLogF(TRACE, "event %s", event.dump(1).c_str());
+    const int eventType = EVENT_MAP.at(type);
+    switch (eventType)
+    {
+      case vtkCommand::EnterEvent:
+      case vtkCommand::LeaveEvent:
+        iren->InvokeEvent(eventType, (void*)&event);
+        break;
+      case vtkCommand::MouseMoveEvent:
+      case vtkCommand::LeftButtonPressEvent:
+      case vtkCommand::LeftButtonReleaseEvent:
+      case vtkCommand::RightButtonPressEvent:
+      case vtkCommand::RightButtonReleaseEvent:
+      case vtkCommand::MiddleButtonPressEvent:
+      case vtkCommand::MiddleButtonReleaseEvent:
+      {
+        const int x =
+          (event.at("x").get<double>() / event.at("w").get<double>() * devicePixelRatio +
+            devicePixelRatioTolerance) *
+          iren->GetRenderWindow()->GetSize()[0];
+        const int y =
+          (event.at("y").get<double>() / event.at("h").get<double>() * devicePixelRatio +
+            devicePixelRatioTolerance) *
+          iren->GetRenderWindow()->GetSize()[1];
+
+        const int ctrlKeyPressed = event.at("ctrlKey").get<int>();
+        const int altKeyPressed = event.at("altKey").get<int>();
+        const int shiftKeyPressed = event.at("shiftKey").get<int>();
+        iren->SetEventInformation(x, y, ctrlKeyPressed, shiftKeyPressed);
+        iren->SetAltKey(altKeyPressed);
+        iren->InvokeEvent(eventType, (void*)&event);
+        break;
+      }
+      case vtkCommand::KeyPressEvent:
+      case vtkCommand::KeyReleaseEvent:
+      {
+        const int ctrlKeyPressed = event.at("controlKey").get<int>();
+        const int altKeyPressed = event.at("altKey").get<int>();
+        const int shiftKeyPressed = event.at("shiftKey").get<int>();
+        const char asciiCode = event.at("keyCode").get<int>();
+        const std::string& key = event.at("key");
+        iren->SetKeyEventInformation(ctrlKeyPressed, shiftKeyPressed, asciiCode, 0, key.c_str());
+        iren->SetAltKey(altKeyPressed);
+        iren->InvokeEvent(eventType);
+        if (eventType == vtkCommand::KeyPressEvent && asciiCode != '\0') // TODO check comparson
+        {
+          iren->InvokeEvent(vtkCommand::CharEvent, (void*)&event);
+        }
+        break;
+      }
+      case WheelEvent:
+      {
+        const int x =
+          (event.at("x").get<double>() / event.at("w").get<double>() * devicePixelRatio +
+            devicePixelRatioTolerance) *
+          iren->GetRenderWindow()->GetSize()[0];
+        const int y =
+          (event.at("y").get<double>() / event.at("h").get<double>() * devicePixelRatio +
+            devicePixelRatioTolerance) *
+          iren->GetRenderWindow()->GetSize()[1];
+
+        const int ctrlKeyPressed = event.at("ctrlKey").get<int>();
+        const int altKeyPressed = event.at("altKey").get<int>();
+        const int shiftKeyPressed = event.at("shiftKey").get<int>();
+
+        iren->SetEventInformation(x, y, ctrlKeyPressed, shiftKeyPressed);
+        iren->SetAltKey(altKeyPressed);
+
+        static double accumulatedDelta = 0;
+        const double verticalDelta = event.at("spinY").get<double>();
+        accumulatedDelta += verticalDelta;
+        const double threshold = 1.0; // in vtk-js the value comes normalized
+
+        // invoke vtk event when accumulated delta passes the threshold
+        // Note: in javascript a forward (away from the user MouseWheelEvent is
+        // indicated with a negative value in contrast to Qt.
+        if (accumulatedDelta <= -threshold && verticalDelta != 0.0)
+        {
+          iren->InvokeEvent(vtkCommand::MouseWheelForwardEvent, (void*)&event);
+          accumulatedDelta = 0;
+        }
+        else if (accumulatedDelta >= threshold && verticalDelta != 0.0)
+        {
+          iren->InvokeEvent(vtkCommand::MouseWheelBackwardEvent, (void*)&event);
+          accumulatedDelta = 0;
+        }
+
+        break;
+      }
+      case vtkCommand::StartPinchEvent:
+      case vtkCommand::EndPinchEvent:
+      case vtkCommand::PinchEvent:
+      case vtkCommand::StartPanEvent:
+      case vtkCommand::EndPanEvent:
+      case vtkCommand::PanEvent:
+      case vtkCommand::StartRotateEvent:
+      case vtkCommand::EndRotateEvent:
+      case vtkCommand::RotateEvent:
+      {
+        // Store event information to restore after gesture is completed
+        int eventPosition[2];
+        iren->GetEventPosition(eventPosition);
+        int lastEventPosition[2];
+        iren->GetLastEventPosition(lastEventPosition);
+
+        // get center of positions for event
+        int position[2] = { 0, 0 };
+        for (const auto& item : event.at("positions"))
+        {
+          position[0] += item.at("x").get<double>() / event.at("w").get<double>();
+          position[1] += item.at("y").get<double>() / event.at("h").get<double>();
+        }
+
+        position[0] /= static_cast<int>(event.at("positions").size());
+        position[1] /= static_cast<int>(event.at("positions").size());
+
+        iren->SetEventInformation(position[0] * devicePixelRatio * devicePixelRatioTolerance,
+          position[1] * devicePixelRatio * devicePixelRatioTolerance);
+
+        if (eventType == vtkCommand::StartPinchEvent || eventType == vtkCommand::EndPinchEvent ||
+          eventType == vtkCommand::PinchEvent)
+        {
+          const double factor = event.at("factor").get<double>();
+          iren->SetScale(1.0);
+          iren->SetScale(factor);
+        }
+        else if (eventType == vtkCommand::StartPanEvent || eventType == vtkCommand::EndPanEvent ||
+          eventType == vtkCommand::PanEvent)
+        {
+          double translation[2] = { event.at("translation").at(0).get<double>(),
+            event.at("translation").at(1).get<double>() };
+          iren->SetTranslation(translation);
+        }
+        else if (eventType == vtkCommand::StartRotateEvent ||
+          eventType == vtkCommand::EndRotateEvent || eventType == vtkCommand::RotateEvent)
+        {
+          const double rotation = event.at("rotation").get<double>();
+          iren->SetRotation(rotation);
+        }
+        else
+        {
+          vtkLogF(ERROR, "Unexpected Event Type");
+          return false;
+        }
+        iren->InvokeEvent(eventType, (void*)&event);
+      }
+      break;
+
+      case vtkCommand::InteractionEvent:
+      case vtkCommand::StartInteractionEvent:
+      case vtkCommand::EndInteractionEvent:
+      case vtkCommand::NoEvent:
+        // nothing to do
+        break;
+      default:
+        vtkLogF(WARNING, "Unhandled event: %s", type.c_str());
+        break;
+    }
+    return true;
+  }
+  catch (std::out_of_range& e)
+  {
+    vtkLogF(ERROR, "Skipping Event: Unknown event type \n%s", e.what());
+    return false;
+  }
+  catch (nlohmann::json::out_of_range& e)
+  {
+    vtkLogF(ERROR, "Skipping Event \n%s", e.what());
+    return false;
+  }
+}
+
+//----------------------------------------------------------------------------
+bool vtkRemoteInteractionAdapter::ProcessEvent(const std::string& event_str)
+{
+  return ProcessEvent(
+    this->Interactor, event_str, this->DevicePixelRatio, this->DevicePixelRatioTolerance);
+}
+
+VTK_ABI_NAMESPACE_END
diff --git a/Web/Core/vtkRemoteInteractionAdapter.h b/Web/Core/vtkRemoteInteractionAdapter.h
new file mode 100644 (file)
index 0000000..e2cb178
--- /dev/null
@@ -0,0 +1,85 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+
+/**
+ * @class vtkRemoteInteractionAdapter
+ * @brief Map vtk-js interaction events to native VTK events
+ *
+ * Apply an vtk-js events to a vtkRenderWindowInteractor.
+ * For the expected format see
+ * https://github.com/Kitware/vtk-js/blob/master/Sources/Interaction/Style/InteractorStyleRemoteMouse/index.js
+ *
+ * Events are processed in the `ProcessEvent` method which can be called
+ * either as a static method providing all the relevant parameters as arguments
+ * or  a class method with the parameters provided via member variables.
+ *
+ */
+
+#ifndef vtkRemoteInteractionAdapter_h
+#define vtkRemoteInteractionAdapter_h
+
+#include "vtkObject.h"
+#include "vtkWebCoreModule.h" // for exports
+
+VTK_ABI_NAMESPACE_BEGIN
+
+class vtkRenderWindowInteractor;
+
+class VTKWEBCORE_EXPORT vtkRemoteInteractionAdapter : public vtkObject
+{
+public:
+  static vtkRemoteInteractionAdapter* New();
+  vtkTypeMacro(vtkRemoteInteractionAdapter, vtkObject);
+  void PrintSelf(ostream& os, vtkIndent indent) override;
+
+  /**
+   * @brief Apply the vtk-js event to the internal RenderWindowInteractor
+   * @param event stringified json representation of a vtk-js interaction event.
+   * @return true if the event is processed , false otherwise
+   */
+  bool ProcessEvent(const std::string& event);
+
+  /**
+   * Static version of ProcessEvent(const std::string&)
+   * @return true if the event is processed , false otherwise
+   */
+  static bool ProcessEvent(vtkRenderWindowInteractor* iren, const std::string& event,
+    double devicePixelRatio = 1.0, double devicePixelRatioTolerance = 1e-5);
+
+  ///@{
+  // Get/Set the  ratio between physical (onscreen) pixel and logical (rendered image)
+  vtkSetMacro(DevicePixelRatio, double);
+  vtkGetMacro(DevicePixelRatio, double);
+  ///@}
+
+  ///@{
+  /**
+   * Tolerance used when truncating the event position from
+   * physical to logical. i.e.  int event_position_x = int(event.at("x") * devicePixelRatio +
+   * devicePixelRatioTolerance)
+   */
+  vtkSetMacro(DevicePixelRatioTolerance, double);
+  vtkGetMacro(DevicePixelRatioTolerance, double);
+  ///@}
+
+  ///@{
+  // Get/Set the Interactor to apply the event to.
+  void SetInteractor(vtkRenderWindowInteractor* iren);
+  vtkGetObjectMacro(Interactor, vtkRenderWindowInteractor);
+  ///@}
+
+protected:
+  vtkRemoteInteractionAdapter();
+  ~vtkRemoteInteractionAdapter() override;
+
+private:
+  vtkRemoteInteractionAdapter(const vtkRemoteInteractionAdapter&) = delete;
+  void operator=(const vtkRemoteInteractionAdapter&) = delete;
+
+  double DevicePixelRatio = 1.0;
+  double DevicePixelRatioTolerance = 1e-5;
+  vtkRenderWindowInteractor* Interactor = nullptr;
+};
+
+VTK_ABI_NAMESPACE_END
+#endif
diff --git a/Web/Core/vtkWebApplication.cxx b/Web/Core/vtkWebApplication.cxx
new file mode 100644 (file)
index 0000000..a8bacb5
--- /dev/null
@@ -0,0 +1,464 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+#include "vtkWebApplication.h"
+
+#include "vtkBase64Utilities.h"
+#include "vtkCamera.h"
+#include "vtkCommand.h"
+#include "vtkDataEncoder.h"
+#include "vtkImageData.h"
+#include "vtkJPEGWriter.h"
+#include "vtkNew.h"
+#include "vtkObjectFactory.h"
+#include "vtkObjectIdMap.h"
+#include "vtkPNGWriter.h"
+#include "vtkPointData.h"
+#include "vtkRenderWindow.h"
+#include "vtkRenderWindowInteractor.h"
+#include "vtkRendererCollection.h"
+#include "vtkSmartPointer.h"
+#include "vtkTimerLog.h"
+#include "vtkUnsignedCharArray.h"
+#include "vtkWebGLExporter.h"
+#include "vtkWebGLObject.h"
+#include "vtkWebInteractionEvent.h"
+#include "vtkWindowToImageFilter.h"
+
+#include <cassert>
+#include <cmath>
+#include <map>
+#include <sstream>
+
+VTK_ABI_NAMESPACE_BEGIN
+class vtkWebApplication::vtkInternals
+{
+public:
+  struct ImageCacheValueType
+  {
+  public:
+    vtkSmartPointer<vtkUnsignedCharArray> Data;
+    bool NeedsRender;
+    bool HasImagesBeingProcessed;
+    vtkObject* ViewPointer;
+    unsigned long ObserverId;
+    ImageCacheValueType()
+      : NeedsRender(true)
+      , HasImagesBeingProcessed(false)
+      , ViewPointer(nullptr)
+      , ObserverId(0)
+    {
+    }
+
+    void SetListener(vtkObject* view)
+    {
+      if (this->ViewPointer == view)
+      {
+        return;
+      }
+
+      if (this->ViewPointer && this->ObserverId)
+      {
+        this->ViewPointer->RemoveObserver(this->ObserverId);
+        this->ObserverId = 0;
+      }
+      this->ViewPointer = view;
+      if (this->ViewPointer)
+      {
+        this->ObserverId = this->ViewPointer->AddObserver(
+          vtkCommand::AnyEvent, this, &ImageCacheValueType::ViewEventListener);
+      }
+    }
+
+    void RemoveListener(vtkObject* view)
+    {
+      if (this->ViewPointer && this->ViewPointer == view && this->ObserverId)
+      {
+        this->ViewPointer->RemoveObserver(this->ObserverId);
+        this->ObserverId = 0;
+        this->ViewPointer = nullptr;
+      }
+    }
+
+    void ViewEventListener(vtkObject*, unsigned long, void*) { this->NeedsRender = true; }
+  };
+  typedef std::map<void*, ImageCacheValueType> ImageCacheType;
+  ImageCacheType ImageCache;
+
+  typedef std::map<void*, unsigned int> ButtonStatesType;
+  ButtonStatesType ButtonStates;
+
+  vtkNew<vtkDataEncoder> Encoder;
+
+  // WebGL related struct
+  struct WebGLObjCacheValue
+  {
+  public:
+    int ObjIndex;
+    std::map<int, std::string> BinaryParts;
+  };
+  // map for <vtkWebGLExporter, <webgl-objID, WebGLObjCacheValue> >
+  typedef std::map<std::string, WebGLObjCacheValue> WebGLObjId2IndexMap;
+  std::map<vtkWebGLExporter*, WebGLObjId2IndexMap> WebGLExporterObjIdMap;
+  // map for <vtkRenderWindow, vtkWebGLExporter>
+  std::map<vtkRenderWindow*, vtkSmartPointer<vtkWebGLExporter>> ViewWebGLMap;
+  std::string LastAllWebGLBinaryObjects;
+  vtkNew<vtkObjectIdMap> ObjectIdMap;
+};
+
+vtkStandardNewMacro(vtkWebApplication);
+//------------------------------------------------------------------------------
+vtkWebApplication::vtkWebApplication()
+  : ImageEncoding(ENCODING_BASE64)
+  , ImageCompression(COMPRESSION_JPEG)
+  , Internals(new vtkWebApplication::vtkInternals())
+{
+}
+
+//------------------------------------------------------------------------------
+vtkWebApplication::~vtkWebApplication()
+{
+  delete this->Internals;
+  this->Internals = nullptr;
+}
+
+//------------------------------------------------------------------------------
+void vtkWebApplication::SetNumberOfEncoderThreads(vtkTypeUInt32 numThreads)
+{
+  this->Internals->Encoder->SetMaxThreads(numThreads);
+  this->Internals->Encoder->Initialize();
+}
+
+//------------------------------------------------------------------------------
+vtkTypeUInt32 vtkWebApplication::GetNumberOfEncoderThreads()
+{
+  return this->Internals->Encoder->GetMaxThreads();
+}
+
+//------------------------------------------------------------------------------
+bool vtkWebApplication::GetHasImagesBeingProcessed(vtkRenderWindow* view)
+{
+  const vtkInternals::ImageCacheValueType& value = this->Internals->ImageCache[view];
+  return value.HasImagesBeingProcessed;
+}
+
+//------------------------------------------------------------------------------
+vtkUnsignedCharArray* vtkWebApplication::InteractiveRender(vtkRenderWindow* view, int quality)
+{
+  // for now, just do the same as StillRender().
+  return this->StillRender(view, quality);
+}
+
+//------------------------------------------------------------------------------
+void vtkWebApplication::InvalidateCache(vtkRenderWindow* view)
+{
+  this->Internals->ImageCache[view].NeedsRender = true;
+}
+
+//------------------------------------------------------------------------------
+vtkUnsignedCharArray* vtkWebApplication::StillRender(vtkRenderWindow* view, int quality)
+{
+  if (!view)
+  {
+    vtkErrorMacro("No view specified.");
+    return nullptr;
+  }
+
+  auto viewID = this->Internals->ObjectIdMap->GetGlobalId(view);
+  vtkInternals::ImageCacheValueType& value = this->Internals->ImageCache[view];
+  value.SetListener(view);
+
+  if (!value.NeedsRender &&
+    value.Data != nullptr /* FIXME SEB &&
+    view->HasDirtyRepresentation() == false */)
+  {
+    bool latest = this->Internals->Encoder->GetLatestOutput(viewID, value.Data);
+    value.HasImagesBeingProcessed = !latest;
+    return value.Data;
+  }
+
+  // cout <<  "Regenerating " << endl;
+  // vtkTimerLog::ResetLog();
+  // vtkTimerLog::CleanupLog();
+  // vtkTimerLog::MarkStartEvent("StillRenderToString");
+  // vtkTimerLog::MarkStartEvent("CaptureWindow");
+
+  view->Render();
+
+  // TODO: We should add logic to check if a new rendering needs to be done and
+  // then alone do a new rendering otherwise use the cached image.
+  vtkNew<vtkWindowToImageFilter> w2i;
+  w2i->SetInput(view);
+  w2i->SetScale(1);
+  w2i->ReadFrontBufferOff();
+  w2i->ShouldRerenderOff();
+  w2i->FixBoundaryOn();
+  w2i->Update();
+
+  auto image = vtkSmartPointer<vtkImageData>::New();
+  image->ShallowCopy(w2i->GetOutput());
+
+  // vtkTimerLog::MarkEndEvent("CaptureWindow");
+
+  // vtkTimerLog::MarkEndEvent("StillRenderToString");
+  // vtkTimerLog::DumpLogWithIndents(&cout, 0.0);
+
+  this->Internals->Encoder->Push(viewID, image, quality, this->ImageEncoding);
+
+  if (value.Data == nullptr)
+  {
+    // we need to wait till output is processed.
+    // cout << "Flushing" << endl;
+    this->Internals->Encoder->Flush(viewID);
+    // cout << "Done Flushing" << endl;
+  }
+
+  bool latest = this->Internals->Encoder->GetLatestOutput(viewID, value.Data);
+  value.HasImagesBeingProcessed = !latest;
+  value.NeedsRender = false;
+  return value.Data;
+}
+
+//------------------------------------------------------------------------------
+const char* vtkWebApplication::StillRenderToString(
+  vtkRenderWindow* view, vtkMTimeType time, int quality)
+{
+  vtkUnsignedCharArray* array = this->StillRender(view, quality);
+  if (array && array->GetMTime() != time)
+  {
+    this->LastStillRenderToMTime = array->GetMTime();
+    // cout << "Image size: " << array->GetNumberOfTuples() << endl;
+    return reinterpret_cast<char*>(array->GetPointer(0));
+  }
+  return nullptr;
+}
+
+//------------------------------------------------------------------------------
+vtkUnsignedCharArray* vtkWebApplication::StillRenderToBuffer(
+  vtkRenderWindow* view, vtkMTimeType time, int quality)
+{
+  vtkUnsignedCharArray* array = this->StillRender(view, quality);
+  if (array && array->GetMTime() != time)
+  {
+    this->LastStillRenderToMTime = array->GetMTime();
+    return array;
+  }
+  return nullptr;
+}
+
+//------------------------------------------------------------------------------
+bool vtkWebApplication::HandleInteractionEvent(vtkRenderWindow* view, vtkWebInteractionEvent* event)
+{
+  vtkRenderWindowInteractor* iren = nullptr;
+
+  if (view)
+  {
+    iren = view->GetInteractor();
+  }
+  else
+  {
+    vtkErrorMacro("Interaction not supported for view : " << view);
+    return false;
+  }
+
+  int ctrlKey = (event->GetModifiers() & vtkWebInteractionEvent::CTRL_KEY) != 0 ? 1 : 0;
+  int shiftKey = (event->GetModifiers() & vtkWebInteractionEvent::SHIFT_KEY) != 0 ? 1 : 0;
+
+  // Handle scroll action if any
+  if (event->GetScroll())
+  {
+    iren->SetEventInformation(0, 0, ctrlKey, shiftKey, event->GetKeyCode(), 0);
+    iren->MouseMoveEvent();
+    iren->RightButtonPressEvent();
+    iren->SetEventInformation(
+      0, event->GetScroll() * 10, ctrlKey, shiftKey, event->GetKeyCode(), 0);
+    iren->MouseMoveEvent();
+    iren->RightButtonReleaseEvent();
+    this->Internals->ImageCache[view].NeedsRender = true;
+    return true;
+  }
+
+  const int* viewSize = view->GetSize();
+  int posX = std::floor(viewSize[0] * event->GetX() + 0.5);
+  int posY = std::floor(viewSize[1] * event->GetY() + 0.5);
+
+  iren->SetEventInformation(
+    posX, posY, ctrlKey, shiftKey, event->GetKeyCode(), event->GetRepeatCount());
+
+  unsigned int prev_buttons = this->Internals->ButtonStates[view];
+  unsigned int changed_buttons = (event->GetButtons() ^ prev_buttons);
+  iren->MouseMoveEvent();
+  if ((changed_buttons & vtkWebInteractionEvent::LEFT_BUTTON) != 0)
+  {
+    if ((event->GetButtons() & vtkWebInteractionEvent::LEFT_BUTTON) != 0)
+    {
+      iren->LeftButtonPressEvent();
+      if (event->GetRepeatCount() > 0)
+      {
+        iren->LeftButtonReleaseEvent();
+      }
+    }
+    else
+    {
+      iren->LeftButtonReleaseEvent();
+    }
+  }
+
+  if ((changed_buttons & vtkWebInteractionEvent::RIGHT_BUTTON) != 0)
+  {
+    if ((event->GetButtons() & vtkWebInteractionEvent::RIGHT_BUTTON) != 0)
+    {
+      iren->RightButtonPressEvent();
+      if (event->GetRepeatCount() > 0)
+      {
+        iren->RightButtonPressEvent();
+      }
+    }
+    else
+    {
+      iren->RightButtonReleaseEvent();
+    }
+  }
+  if ((changed_buttons & vtkWebInteractionEvent::MIDDLE_BUTTON) != 0)
+  {
+    if ((event->GetButtons() & vtkWebInteractionEvent::MIDDLE_BUTTON) != 0)
+    {
+      iren->MiddleButtonPressEvent();
+      if (event->GetRepeatCount() > 0)
+      {
+        iren->MiddleButtonPressEvent();
+      }
+    }
+    else
+    {
+      iren->MiddleButtonReleaseEvent();
+    }
+  }
+
+  this->Internals->ButtonStates[view] = event->GetButtons();
+
+  bool needs_render = (changed_buttons != 0 || event->GetButtons());
+  this->Internals->ImageCache[view].NeedsRender = needs_render;
+  return needs_render;
+}
+
+//------------------------------------------------------------------------------
+const char* vtkWebApplication::GetWebGLSceneMetaData(vtkRenderWindow* view)
+{
+  if (!view)
+  {
+    vtkErrorMacro("No view specified.");
+    return nullptr;
+  }
+
+  // We use the camera focal point to be the center of rotation
+  double centerOfRotation[3];
+  vtkCamera* cam = view->GetRenderers()->GetFirstRenderer()->GetActiveCamera();
+  cam->GetFocalPoint(centerOfRotation);
+
+  if (this->Internals->ViewWebGLMap.find(view) == this->Internals->ViewWebGLMap.end())
+  {
+    this->Internals->ViewWebGLMap[view] = vtkSmartPointer<vtkWebGLExporter>::New();
+  }
+
+  std::stringstream globalIdAsString;
+  globalIdAsString << this->Internals->ObjectIdMap->GetGlobalId(view);
+
+  vtkWebGLExporter* webglExporter = this->Internals->ViewWebGLMap[view];
+  webglExporter->parseScene(view->GetRenderers(), globalIdAsString.str().c_str(), VTK_PARSEALL);
+
+  vtkInternals::WebGLObjId2IndexMap webglMap;
+  for (int i = 0; i < webglExporter->GetNumberOfObjects(); ++i)
+  {
+    vtkWebGLObject* wObj = webglExporter->GetWebGLObject(i);
+    if (wObj && wObj->isVisible())
+    {
+      vtkInternals::WebGLObjCacheValue val;
+      val.ObjIndex = i;
+      for (int j = 0; j < wObj->GetNumberOfParts(); ++j)
+      {
+        val.BinaryParts[j] = "";
+      }
+      webglMap[wObj->GetId()] = val;
+    }
+  }
+  this->Internals->WebGLExporterObjIdMap[webglExporter] = webglMap;
+  webglExporter->SetCenterOfRotation(static_cast<float>(centerOfRotation[0]),
+    static_cast<float>(centerOfRotation[1]), static_cast<float>(centerOfRotation[2]));
+  return webglExporter->GenerateMetadata();
+}
+
+//------------------------------------------------------------------------------
+const char* vtkWebApplication::GetWebGLBinaryData(vtkRenderWindow* view, const char* id, int part)
+{
+  if (!view)
+  {
+    vtkErrorMacro("No view specified.");
+    return nullptr;
+  }
+  if (this->Internals->ViewWebGLMap.find(view) == this->Internals->ViewWebGLMap.end())
+  {
+    if (this->GetWebGLSceneMetaData(view) == nullptr)
+    {
+      vtkErrorMacro("Failed to generate WebGL MetaData for: " << view);
+      return nullptr;
+    }
+  }
+
+  vtkWebGLExporter* webglExporter = this->Internals->ViewWebGLMap[view];
+  if (webglExporter == nullptr)
+  {
+    vtkErrorMacro("There is no cached WebGL Exporter for: " << view);
+    return nullptr;
+  }
+
+  if (!this->Internals->WebGLExporterObjIdMap[webglExporter].empty() &&
+    this->Internals->WebGLExporterObjIdMap[webglExporter].find(id) !=
+      this->Internals->WebGLExporterObjIdMap[webglExporter].end())
+  {
+    vtkInternals::WebGLObjCacheValue* cachedVal =
+      &(this->Internals->WebGLExporterObjIdMap[webglExporter][id]);
+    if (cachedVal->BinaryParts.find(part) != cachedVal->BinaryParts.end())
+    {
+      if (cachedVal->BinaryParts[part].empty())
+      {
+        vtkWebGLObject* obj = webglExporter->GetWebGLObject(cachedVal->ObjIndex);
+        if (obj && obj->isVisible())
+        {
+          // Manage Base64
+          vtkNew<vtkBase64Utilities> base64;
+          unsigned char* output = new unsigned char[obj->GetBinarySize(part) * 2];
+          int size =
+            base64->Encode(obj->GetBinaryData(part), obj->GetBinarySize(part), output, false);
+          cachedVal->BinaryParts[part] = std::string((const char*)output, size);
+          delete[] output;
+        }
+      }
+      return cachedVal->BinaryParts[part].c_str();
+    }
+  }
+
+  return nullptr;
+}
+
+//------------------------------------------------------------------------------
+void vtkWebApplication::PrintSelf(ostream& os, vtkIndent indent)
+{
+  this->Superclass::PrintSelf(os, indent);
+  os << indent << "ImageEncoding: " << this->ImageEncoding << endl;
+  os << indent << "ImageCompression: " << this->ImageCompression << endl;
+}
+
+//------------------------------------------------------------------------------
+vtkObjectIdMap* vtkWebApplication::GetObjectIdMap()
+{
+  return this->Internals->ObjectIdMap;
+}
+
+//------------------------------------------------------------------------------
+std::string vtkWebApplication::GetObjectId(vtkObject* obj)
+{
+  std::ostringstream oss;
+  oss << std::hex << static_cast<void*>(obj);
+  return oss.str();
+}
+VTK_ABI_NAMESPACE_END
diff --git a/Web/Core/vtkWebApplication.h b/Web/Core/vtkWebApplication.h
new file mode 100644 (file)
index 0000000..80b9a10
--- /dev/null
@@ -0,0 +1,144 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+/**
+ * @class   vtkWebApplication
+ * @brief   defines ParaViewWeb application interface.
+ *
+ * vtkWebApplication defines the core interface for a ParaViewWeb application.
+ * This exposes methods that make it easier to manage views and rendered images
+ * from views.
+ */
+
+#ifndef vtkWebApplication_h
+#define vtkWebApplication_h
+
+#include "vtkObject.h"
+#include "vtkWebCoreModule.h" // needed for exports
+#include <string>             // needed for std::string
+
+VTK_ABI_NAMESPACE_BEGIN
+class vtkObjectIdMap;
+class vtkRenderWindow;
+class vtkUnsignedCharArray;
+class vtkWebInteractionEvent;
+
+class VTKWEBCORE_EXPORT vtkWebApplication : public vtkObject
+{
+public:
+  static vtkWebApplication* New();
+  vtkTypeMacro(vtkWebApplication, vtkObject);
+  void PrintSelf(ostream& os, vtkIndent indent) override;
+
+  ///@{
+  /**
+   * Set the encoding to be used for rendered images.
+   */
+  enum
+  {
+    ENCODING_NONE = 0,
+    ENCODING_BASE64 = 1
+  };
+  vtkSetClampMacro(ImageEncoding, int, ENCODING_NONE, ENCODING_BASE64);
+  vtkGetMacro(ImageEncoding, int);
+  ///@}
+
+  ///@{
+  /**
+   * Set the compression to be used for rendered images.
+   */
+  enum
+  {
+    COMPRESSION_NONE = 0,
+    COMPRESSION_PNG = 1,
+    COMPRESSION_JPEG = 2
+  };
+  vtkSetClampMacro(ImageCompression, int, COMPRESSION_NONE, COMPRESSION_JPEG);
+  vtkGetMacro(ImageCompression, int);
+  ///@}
+
+  ///@{
+  /**
+   * Set the number of worker threads to use for image encoding.  Calling this
+   * method with a number greater than 32 or less than zero will have no effect.
+   */
+  void SetNumberOfEncoderThreads(vtkTypeUInt32);
+  vtkTypeUInt32 GetNumberOfEncoderThreads();
+  ///@}
+
+  ///@{
+  /**
+   * Render a view and obtain the rendered image.
+   */
+  vtkUnsignedCharArray* StillRender(vtkRenderWindow* view, int quality = 100);
+  vtkUnsignedCharArray* InteractiveRender(vtkRenderWindow* view, int quality = 50);
+  const char* StillRenderToString(vtkRenderWindow* view, vtkMTimeType time = 0, int quality = 100);
+  vtkUnsignedCharArray* StillRenderToBuffer(
+    vtkRenderWindow* view, vtkMTimeType time = 0, int quality = 100);
+  ///@}
+
+  /**
+   * StillRenderToString() need not necessary returns the most recently rendered
+   * image. Use this method to get whether there are any pending images being
+   * processed concurrently.
+   */
+  bool GetHasImagesBeingProcessed(vtkRenderWindow*);
+
+  /**
+   * Communicate mouse interaction to a view.
+   * Returns true if the interaction changed the view state, otherwise returns false.
+   */
+  bool HandleInteractionEvent(vtkRenderWindow* view, vtkWebInteractionEvent* event);
+
+  /**
+   * Invalidate view cache
+   */
+  void InvalidateCache(vtkRenderWindow* view);
+
+  ///@{
+  /**
+   * Return the MTime of the last array exported by StillRenderToString.
+   */
+  vtkGetMacro(LastStillRenderToMTime, vtkMTimeType);
+  ///@}
+
+  /**
+   * Return the Meta data description of the input scene in JSON format.
+   * This is using the vtkWebGLExporter to parse the scene.
+   * NOTE: This should be called before getting the webGL binary data.
+   */
+  const char* GetWebGLSceneMetaData(vtkRenderWindow* view);
+
+  /**
+   * Return the binary data given the part index
+   * and the webGL object piece id in the scene.
+   */
+  const char* GetWebGLBinaryData(vtkRenderWindow* view, const char* id, int partIndex);
+
+  vtkObjectIdMap* GetObjectIdMap();
+
+  /**
+   * Return a hexadecimal formatted string of the VTK object's memory address,
+   * useful for uniquely identifying the object when exporting data.
+   *
+   * e.g. 0x8f05a90
+   */
+  static std::string GetObjectId(vtkObject* obj);
+
+protected:
+  vtkWebApplication();
+  ~vtkWebApplication() override;
+
+  int ImageEncoding;
+  int ImageCompression;
+  vtkMTimeType LastStillRenderToMTime;
+
+private:
+  vtkWebApplication(const vtkWebApplication&) = delete;
+  void operator=(const vtkWebApplication&) = delete;
+
+  class vtkInternals;
+  vtkInternals* Internals;
+};
+
+VTK_ABI_NAMESPACE_END
+#endif
diff --git a/Web/Core/vtkWebInteractionEvent.cxx b/Web/Core/vtkWebInteractionEvent.cxx
new file mode 100644 (file)
index 0000000..bc988fe
--- /dev/null
@@ -0,0 +1,36 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+#include "vtkWebInteractionEvent.h"
+
+#include "vtkObjectFactory.h"
+
+VTK_ABI_NAMESPACE_BEGIN
+vtkStandardNewMacro(vtkWebInteractionEvent);
+//------------------------------------------------------------------------------
+vtkWebInteractionEvent::vtkWebInteractionEvent()
+  : Buttons(0)
+  , Modifiers(0)
+  , KeyCode(0)
+  , X(0.0)
+  , Y(0.0)
+  , Scroll(0.0)
+  , RepeatCount(0)
+{
+}
+
+//------------------------------------------------------------------------------
+vtkWebInteractionEvent::~vtkWebInteractionEvent() = default;
+
+//------------------------------------------------------------------------------
+void vtkWebInteractionEvent::PrintSelf(ostream& os, vtkIndent indent)
+{
+  this->Superclass::PrintSelf(os, indent);
+  os << indent << "Buttons: " << this->Buttons << endl;
+  os << indent << "Modifiers: " << this->Modifiers << endl;
+  os << indent << "KeyCode: " << static_cast<int>(this->KeyCode) << endl;
+  os << indent << "X: " << this->X << endl;
+  os << indent << "Y: " << this->Y << endl;
+  os << indent << "RepeatCount: " << this->RepeatCount << endl;
+  os << indent << "Scroll: " << this->Scroll << endl;
+}
+VTK_ABI_NAMESPACE_END
diff --git a/Web/Core/vtkWebInteractionEvent.h b/Web/Core/vtkWebInteractionEvent.h
new file mode 100644 (file)
index 0000000..e088ccf
--- /dev/null
@@ -0,0 +1,96 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+/**
+ * @class   vtkWebInteractionEvent
+ *
+ *
+ */
+
+#ifndef vtkWebInteractionEvent_h
+#define vtkWebInteractionEvent_h
+
+#include "vtkObject.h"
+#include "vtkWebCoreModule.h" // needed for exports
+
+VTK_ABI_NAMESPACE_BEGIN
+class VTKWEBCORE_EXPORT vtkWebInteractionEvent : public vtkObject
+{
+public:
+  static vtkWebInteractionEvent* New();
+  vtkTypeMacro(vtkWebInteractionEvent, vtkObject);
+  void PrintSelf(ostream& os, vtkIndent indent) override;
+
+  enum MouseButton
+  {
+    LEFT_BUTTON = 0x01,
+    MIDDLE_BUTTON = 0x02,
+    RIGHT_BUTTON = 0x04
+  };
+
+  enum ModifierKeys
+  {
+    SHIFT_KEY = 0x01,
+    CTRL_KEY = 0x02,
+    ALT_KEY = 0x04,
+    META_KEY = 0x08
+  };
+
+  ///@{
+  /**
+   * Set/Get the mouse buttons state.
+   */
+  vtkSetMacro(Buttons, unsigned int);
+  vtkGetMacro(Buttons, unsigned int);
+  ///@}
+
+  ///@{
+  /**
+   * Set/Get modifier state.
+   */
+  vtkSetMacro(Modifiers, unsigned int);
+  vtkGetMacro(Modifiers, unsigned int);
+  ///@}
+
+  ///@{
+  /**
+   * Set/Get the chart code.
+   */
+  vtkSetMacro(KeyCode, char);
+  vtkGetMacro(KeyCode, char);
+  ///@}
+
+  ///@{
+  /**
+   * Set/Get event position.
+   */
+  vtkSetMacro(X, double);
+  vtkGetMacro(X, double);
+  vtkSetMacro(Y, double);
+  vtkGetMacro(Y, double);
+  vtkSetMacro(Scroll, double);
+  vtkGetMacro(Scroll, double);
+  ///@}
+
+  // Handle double click
+  vtkSetMacro(RepeatCount, int);
+  vtkGetMacro(RepeatCount, int);
+
+protected:
+  vtkWebInteractionEvent();
+  ~vtkWebInteractionEvent() override;
+
+  unsigned int Buttons;
+  unsigned int Modifiers;
+  char KeyCode;
+  double X;
+  double Y;
+  double Scroll;
+  int RepeatCount;
+
+private:
+  vtkWebInteractionEvent(const vtkWebInteractionEvent&) = delete;
+  void operator=(const vtkWebInteractionEvent&) = delete;
+};
+
+VTK_ABI_NAMESPACE_END
+#endif
diff --git a/Web/Core/vtkWebUtilities.cxx b/Web/Core/vtkWebUtilities.cxx
new file mode 100644 (file)
index 0000000..b06c31a
--- /dev/null
@@ -0,0 +1,118 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+#include "vtkWebUtilities.h"
+#include "vtkPython.h" // Need to be first and used for Py_xxx macros
+
+#include "vtkDataSet.h"
+#include "vtkDataSetAttributes.h"
+#include "vtkJavaScriptDataWriter.h"
+#include "vtkMultiProcessController.h"
+#include "vtkNew.h"
+#include "vtkObjectFactory.h"
+#include "vtkSplitColumnComponents.h"
+#include "vtkTable.h"
+
+#include <sstream>
+
+VTK_ABI_NAMESPACE_BEGIN
+vtkStandardNewMacro(vtkWebUtilities);
+//------------------------------------------------------------------------------
+vtkWebUtilities::vtkWebUtilities() = default;
+
+//------------------------------------------------------------------------------
+vtkWebUtilities::~vtkWebUtilities() = default;
+
+//------------------------------------------------------------------------------
+std::string vtkWebUtilities::WriteAttributesToJavaScript(int field_type, vtkDataSet* dataset)
+{
+  if (dataset == nullptr ||
+    (field_type != vtkDataObject::POINT && field_type != vtkDataObject::CELL))
+  {
+    return "[]";
+  }
+
+  std::ostringstream stream;
+
+  vtkNew<vtkDataSetAttributes> clone;
+  clone->PassData(dataset->GetAttributes(field_type));
+  clone->RemoveArray("vtkValidPointMask");
+
+  vtkNew<vtkTable> table;
+  table->SetRowData(clone);
+
+  vtkNew<vtkSplitColumnComponents> splitter;
+  splitter->SetInputDataObject(table);
+  splitter->Update();
+
+  vtkNew<vtkJavaScriptDataWriter> writer;
+  writer->SetOutputStream(&stream);
+  writer->SetInputDataObject(splitter->GetOutputDataObject(0));
+  writer->SetVariableName(nullptr);
+  writer->SetIncludeFieldNames(false);
+  writer->Write();
+
+  return stream.str();
+}
+
+//------------------------------------------------------------------------------
+std::string vtkWebUtilities::WriteAttributeHeadersToJavaScript(int field_type, vtkDataSet* dataset)
+{
+  if (dataset == nullptr ||
+    (field_type != vtkDataObject::POINT && field_type != vtkDataObject::CELL))
+  {
+    return "[]";
+  }
+
+  std::ostringstream stream;
+  stream << "[";
+
+  vtkDataSetAttributes* dsa = dataset->GetAttributes(field_type);
+  vtkNew<vtkDataSetAttributes> clone;
+  clone->CopyAllocate(dsa, 0);
+  clone->RemoveArray("vtkValidPointMask");
+
+  vtkNew<vtkTable> table;
+  table->SetRowData(clone);
+
+  vtkNew<vtkSplitColumnComponents> splitter;
+  splitter->SetInputDataObject(table);
+  splitter->Update();
+
+  dsa = vtkTable::SafeDownCast(splitter->GetOutputDataObject(0))->GetRowData();
+
+  for (int cc = 0; cc < dsa->GetNumberOfArrays(); cc++)
+  {
+    const char* name = dsa->GetArrayName(cc);
+    if (cc != 0)
+    {
+      stream << ", ";
+    }
+    stream << "\"" << (name ? name : "") << "\"";
+  }
+  stream << "]";
+  return stream.str();
+}
+
+//------------------------------------------------------------------------------
+void vtkWebUtilities::PrintSelf(ostream& os, vtkIndent indent)
+{
+  this->Superclass::PrintSelf(os, indent);
+}
+
+//------------------------------------------------------------------------------
+void vtkWebUtilities::ProcessRMIs()
+{
+  vtkWebUtilities::ProcessRMIs(1, 0);
+}
+
+//------------------------------------------------------------------------------
+void vtkWebUtilities::ProcessRMIs(int reportError, int dont_loop)
+{
+  Py_BEGIN_ALLOW_THREADS
+
+  vtkMultiProcessController::GetGlobalController()
+    ->ProcessRMIs(reportError, dont_loop);
+
+  Py_END_ALLOW_THREADS
+}
+VTK_ABI_NAMESPACE_END
diff --git a/Web/Core/vtkWebUtilities.h b/Web/Core/vtkWebUtilities.h
new file mode 100644 (file)
index 0000000..2b76ef9
--- /dev/null
@@ -0,0 +1,52 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+/**
+ * @class   vtkWebUtilities
+ * @brief   collection of utility functions for ParaView Web.
+ *
+ * vtkWebUtilities consolidates miscellaneous utility functions useful for
+ * Python scripts designed for ParaView Web.
+ */
+
+#ifndef vtkWebUtilities_h
+#define vtkWebUtilities_h
+
+#include "vtkObject.h"
+#include "vtkWebCoreModule.h" // needed for exports
+#include <string>             // for std::string
+
+VTK_ABI_NAMESPACE_BEGIN
+class vtkDataSet;
+
+class VTKWEBCORE_EXPORT vtkWebUtilities : public vtkObject
+{
+public:
+  static vtkWebUtilities* New();
+  vtkTypeMacro(vtkWebUtilities, vtkObject);
+  void PrintSelf(ostream& os, vtkIndent indent) override;
+
+  static std::string WriteAttributesToJavaScript(int field_type, vtkDataSet*);
+  static std::string WriteAttributeHeadersToJavaScript(int field_type, vtkDataSet*);
+
+  ///@{
+  /**
+   * This method is similar to the ProcessRMIs() method on the GlobalController
+   * except that it is Python friendly in the sense that it will release the
+   * Python GIS lock, so when run in a thread, this will truly work in the
+   * background without locking the main one.
+   */
+  static void ProcessRMIs();
+  static void ProcessRMIs(int reportError, int dont_loop = 0);
+  ///@}
+
+protected:
+  vtkWebUtilities();
+  ~vtkWebUtilities() override;
+
+private:
+  vtkWebUtilities(const vtkWebUtilities&) = delete;
+  void operator=(const vtkWebUtilities&) = delete;
+};
+
+VTK_ABI_NAMESPACE_END
+#endif
diff --git a/Web/Python/CMakeLists.txt b/Web/Python/CMakeLists.txt
new file mode 100644 (file)
index 0000000..5254e77
--- /dev/null
@@ -0,0 +1,25 @@
+set(files
+  vtkmodules/web/__init__.py
+  vtkmodules/web/camera.py
+  vtkmodules/web/dataset_builder.py
+  vtkmodules/web/errors.py
+  vtkmodules/web/protocols.py
+  vtkmodules/web/query_data_model.py
+  vtkmodules/web/render_window_serializer.py
+  vtkmodules/web/testing.py
+  vtkmodules/web/vtkjs_helper.py
+  vtkmodules/web/venv.py
+  vtkmodules/web/wslink.py
+  vtkmodules/web/utils.py)
+
+vtk_module_add_python_package(VTK::WebPython
+  FILES               ${files}
+  PACKAGE             "vtkmodules.web"
+  MODULE_DESTINATION  "${VTK_PYTHON_SITE_PACKAGES_SUFFIX}")
+
+vtk_module_add_python_module(VTK::WebPython
+  PACKAGES "vtkmodules.web")
+
+set_property(GLOBAL APPEND
+  PROPERTY
+    vtk_web_python_modules "wslink>=1.0.4")
diff --git a/Web/Python/Testing/CMakeLists.txt b/Web/Python/Testing/CMakeLists.txt
new file mode 100644 (file)
index 0000000..e33473e
--- /dev/null
@@ -0,0 +1,3 @@
+if (VTK_WRAP_PYTHON)
+  add_subdirectory(Python)
+endif ()
diff --git a/Web/Python/Testing/Python/CMakeLists.txt b/Web/Python/Testing/Python/CMakeLists.txt
new file mode 100644 (file)
index 0000000..44d6482
--- /dev/null
@@ -0,0 +1,4 @@
+vtk_add_test_python(
+  NO_DATA NO_VALID NO_OUTPUT
+  TestSerializeRenderWindow.py
+  )
diff --git a/Web/Python/Testing/Python/TestSerializeRenderWindow.py b/Web/Python/Testing/Python/TestSerializeRenderWindow.py
new file mode 100644 (file)
index 0000000..933675a
--- /dev/null
@@ -0,0 +1,43 @@
+import json
+from vtkmodules.vtkFiltersSources import vtkConeSource
+from vtkmodules.vtkRenderingCore import (
+    vtkActor,
+    vtkPolyDataMapper,
+    vtkRenderWindow,
+    vtkRenderer,
+)
+import vtkmodules.vtkRenderingFreeType
+import vtkmodules.vtkRenderingOpenGL2
+from vtkmodules.web import render_window_serializer as rws
+from vtkmodules.test import Testing
+
+class TestSerializeRenderWindow(Testing.vtkTest):
+    def testSerializeRenderWindow(self):
+        cone = vtkConeSource()
+
+        coneMapper = vtkPolyDataMapper()
+        coneMapper.SetInputConnection(cone.GetOutputPort())
+
+        coneActor = vtkActor()
+        coneActor.SetMapper(coneMapper)
+
+        ren = vtkRenderer()
+        ren.AddActor(coneActor)
+        renWin = vtkRenderWindow()
+        renWin.AddRenderer(ren)
+
+        ren.ResetCamera()
+        renWin.Render()
+
+        # Exercise some of the serialization functionality and make sure it
+        # does not generate a stack trace
+        context = rws.SynchronizationContext()
+        rws.initializeSerializers()
+        jsonData = rws.serializeInstance(None, renWin, rws.getReferenceId(renWin), context, 0)
+
+        # jsonStr = json.dumps(jsonData)
+        # print jsonStr
+        # print len(jsonStr)
+
+if __name__ == "__main__":
+    Testing.main([(TestSerializeRenderWindow, 'test')])
diff --git a/Web/Python/vtk.module b/Web/Python/vtk.module
new file mode 100644 (file)
index 0000000..f5b04d5
--- /dev/null
@@ -0,0 +1,22 @@
+NAME
+  VTK::WebPython
+LIBRARY_NAME
+  vtkWebPython
+CONDITION
+  VTK_WRAP_PYTHON
+GROUPS
+  Web
+SPDX_LICENSE_IDENTIFIER
+  BSD-3-Clause
+SPDX_COPYRIGHT_TEXT
+  Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+DEPENDS
+  VTK::CommonCore
+PRIVATE_DEPENDS
+  VTK::FiltersGeometry
+  VTK::WebCore
+TEST_DEPENDS
+  VTK::TestingCore
+TEST_LABELS
+  VTK::Web
+EXCLUDE_WRAP
diff --git a/Web/Python/vtkmodules/web/__init__.py b/Web/Python/vtkmodules/web/__init__.py
new file mode 100644 (file)
index 0000000..60e9ae8
--- /dev/null
@@ -0,0 +1,62 @@
+import hashlib, base64
+
+arrayTypesMapping = [
+    " ",  # VTK_VOID            0
+    " ",  # VTK_BIT             1
+    "b",  # VTK_CHAR            2
+    "B",  # VTK_UNSIGNED_CHAR   3
+    "h",  # VTK_SHORT           4
+    "H",  # VTK_UNSIGNED_SHORT  5
+    "i",  # VTK_INT             6
+    "I",  # VTK_UNSIGNED_INT    7
+    "l",  # VTK_LONG            8
+    "L",  # VTK_UNSIGNED_LONG   9
+    "f",  # VTK_FLOAT          10
+    "d",  # VTK_DOUBLE         11
+    "L",  # VTK_ID_TYPE        12
+    " ",  # unspecified        13
+    " ",  # unspecified        14
+    "b",  # signed_char        15
+]
+
+javascriptMapping = {
+    "b": "Int8Array",
+    "B": "Uint8Array",
+    "h": "Int16Array",
+    "H": "Int16Array",
+    "i": "Int32Array",
+    "I": "Uint32Array",
+    "l": "Int32Array",
+    "L": "Uint32Array",
+    "f": "Float32Array",
+    "d": "Float64Array",
+}
+
+
+def iteritems(d, **kwargs):
+    return iter(d.items(**kwargs))
+
+
+def base64Encode(x):
+    return base64.b64encode(x).decode("utf-8")
+
+
+def hashDataArray(dataArray):
+    hashedBit = hashlib.md5(memoryview(dataArray)).hexdigest()
+    typeCode = arrayTypesMapping[dataArray.GetDataType()]
+    return "%s_%d%s" % (hashedBit, dataArray.GetSize(), typeCode)
+
+
+def getJSArrayType(dataArray):
+    return javascriptMapping[arrayTypesMapping[dataArray.GetDataType()]]
+
+
+def getReferenceId(ref):
+    if ref:
+        try:
+            return ref.__this__[1:17]
+        except:
+            idStr = str(ref)[-12:-1]
+            # print('====> fallback ID %s for %s' % (idStr, ref))
+            return idStr
+    return "0x0"
diff --git a/Web/Python/vtkmodules/web/camera.py b/Web/Python/vtkmodules/web/camera.py
new file mode 100644 (file)
index 0000000..bd706b1
--- /dev/null
@@ -0,0 +1,640 @@
+from math import *
+
+# -----------------------------------------------------------------------------
+# Set of helper functions
+# -----------------------------------------------------------------------------
+
+
+def normalize(vect, tolerance=0.00001):
+    mag2 = sum(n * n for n in vect)
+    if abs(mag2 - 1.0) > tolerance:
+        mag = sqrt(mag2)
+        vect = tuple(n / mag for n in vect)
+    return vect
+
+
+def q_mult(q1, q2):
+    w1, x1, y1, z1 = q1
+    w2, x2, y2, z2 = q2
+    w = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2
+    x = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2
+    y = w1 * y2 + y1 * w2 + z1 * x2 - x1 * z2
+    z = w1 * z2 + z1 * w2 + x1 * y2 - y1 * x2
+    return w, x, y, z
+
+
+def q_conjugate(q):
+    w, x, y, z = q
+    return (w, -x, -y, -z)
+
+
+def qv_mult(q1, v1):
+    q2 = (0.0,) + v1
+    return q_mult(q_mult(q1, q2), q_conjugate(q1))[1:]
+
+
+def axisangle_to_q(v, theta):
+    v = normalize(v)
+    x, y, z = v
+    theta /= 2
+    w = cos(theta)
+    x = x * sin(theta)
+    y = y * sin(theta)
+    z = z * sin(theta)
+    return w, x, y, z
+
+
+def vectProduct(axisA, axisB):
+    xa, ya, za = axisA
+    xb, yb, zb = axisB
+    normalVect = (ya * zb - za * yb, za * xb - xa * zb, xa * yb - ya * xb)
+    normalVect = normalize(normalVect)
+    return normalVect
+
+
+def dotProduct(vecA, vecB):
+    return (vecA[0] * vecB[0]) + (vecA[1] * vecB[1]) + (vecA[2] * vecB[2])
+
+
+def rotate(axis, angle, center, point):
+    angleInRad = 3.141592654 * angle / 180.0
+    rotation = axisangle_to_q(axis, angleInRad)
+    tPoint = tuple((point[i] - center[i]) for i in range(3))
+    rtPoint = qv_mult(rotation, tPoint)
+    rPoint = tuple((rtPoint[i] + center[i]) for i in range(3))
+    return rPoint
+
+
+# -----------------------------------------------------------------------------
+# Spherical Camera
+# -----------------------------------------------------------------------------
+
+
+class SphericalCamera(object):
+    def __init__(
+        self, dataHandler, focalPoint, position, phiAxis, phiAngles, thetaAngles
+    ):
+        self.dataHandler = dataHandler
+        self.cameraSettings = []
+        self.thetaBind = {
+            "mouse": {
+                "drag": {"modifier": 0, "coordinate": 1, "step": 30, "orientation": 1}
+            }
+        }
+        self.phiBind = {
+            "mouse": {
+                "drag": {"modifier": 0, "coordinate": 0, "step": 30, "orientation": 1}
+            }
+        }
+
+        # Convert to serializable type
+        fp = tuple(i for i in focalPoint)
+
+        # Register arguments to the data handler
+        if len(phiAngles) > 1 and phiAngles[-1] + phiAngles[1] == 360:
+            self.dataHandler.registerArgument(
+                priority=0,
+                name="phi",
+                values=phiAngles,
+                ui="slider",
+                loop="modulo",
+                bind=self.phiBind,
+            )
+        else:
+            self.dataHandler.registerArgument(
+                priority=0, name="phi", values=phiAngles, ui="slider", bind=self.phiBind
+            )
+        if thetaAngles[0] < 0 and thetaAngles[0] >= -90:
+            idx = 0
+            for theta in thetaAngles:
+                if theta < 0:
+                    idx += 1
+
+            self.dataHandler.registerArgument(
+                priority=0,
+                name="theta",
+                values=[(x + 90) for x in thetaAngles],
+                ui="slider",
+                default=idx,
+                bind=self.thetaBind,
+            )
+        else:
+            self.dataHandler.registerArgument(
+                priority=0,
+                name="theta",
+                values=thetaAngles,
+                ui="slider",
+                bind=self.thetaBind,
+            )
+
+        # Compute all camera settings
+        for theta in thetaAngles:
+            for phi in phiAngles:
+                phiPos = rotate(phiAxis, -phi, fp, position)
+                thetaAxis = vectProduct(
+                    phiAxis, tuple(fp[i] - phiPos[i] for i in range(3))
+                )
+                thetaPhiPos = rotate(thetaAxis, theta, fp, phiPos)
+                viewUp = rotate(thetaAxis, theta, (0, 0, 0), phiAxis)
+
+                self.cameraSettings.append(
+                    {
+                        "theta": theta,
+                        "thetaIdx": thetaAngles.index(theta),
+                        "phi": phi,
+                        "phiIdx": phiAngles.index(phi),
+                        "focalPoint": fp,
+                        "position": thetaPhiPos,
+                        "viewUp": viewUp,
+                    }
+                )
+
+        self.dataHandler.updateBasePattern()
+
+    def updatePriority(self, priorityList):
+        keyList = ["theta", "phi"]
+        for idx in range(min(len(priorityList), len(keyList))):
+            self.dataHandler.updatePriority(keyList[idx], priorityList[idx])
+
+    def __iter__(self):
+        for cameraData in self.cameraSettings:
+            self.dataHandler.setArguments(
+                phi=cameraData["phiIdx"], theta=cameraData["thetaIdx"]
+            )
+            yield cameraData
+
+
+# -----------------------------------------------------------------------------
+# Cylindrical Camera
+# -----------------------------------------------------------------------------
+
+
+class CylindricalCamera(object):
+    def __init__(
+        self,
+        dataHandler,
+        focalPoint,
+        position,
+        rotationAxis,
+        phiAngles,
+        translationValues,
+    ):
+        self.dataHandler = dataHandler
+        self.cameraSettings = []
+
+        # Register arguments to the data handler
+        self.dataHandler.registerArgument(
+            priority=0, name="phi", values=phiAngles, ui="slider", loop="modulo"
+        )
+        self.dataHandler.registerArgument(
+            priority=0, name="n_pos", values=translationValues, ui="slider"
+        )
+
+        # Compute all camera settings
+        for translation in translationValues:
+            for phi in phiAngles:
+                phiPos = rotate(rotationAxis, phi, focalPoint, position)
+                newfocalPoint = tuple(
+                    focalPoint[i] + (translation * rotationAxis[i]) for i in range(3)
+                )
+                transPhiPoint = tuple(
+                    phiPos[i] + (translation * rotationAxis[i]) for i in range(3)
+                )
+
+                self.cameraSettings.append(
+                    {
+                        "n_pos": translation,
+                        "n_posIdx": translationValues.index(translation),
+                        "phi": phi,
+                        "phiIdx": phiAngles.index(phi),
+                        "focalPoint": newfocalPoint,
+                        "position": transPhiPoint,
+                        "viewUp": rotationAxis,
+                    }
+                )
+
+        self.dataHandler.updateBasePattern()
+
+    def updatePriority(self, priorityList):
+        keyList = ["n_pos", "phi"]
+        for idx in range(min(len(priorityList), len(keyList))):
+            self.dataHandler.updatePriority(keyList[idx], priorityList[idx])
+
+    def __iter__(self):
+        for cameraData in self.cameraSettings:
+            self.dataHandler.setArguments(
+                phi=cameraData["phiIdx"], n_pos=cameraData["n_posIdx"]
+            )
+            yield cameraData
+
+
+# -----------------------------------------------------------------------------
+# MultiView Cube Camera
+# -----------------------------------------------------------------------------
+
+
+class CubeCamera(object):
+
+    # positions = [ { position: [x,y,z], args: { i: 1, j: 0, k: 7 } }, ... ]
+    def __init__(self, dataHandler, viewForward, viewUp, positions):
+        self.dataHandler = dataHandler
+        self.cameraSettings = []
+        self.viewForward = viewForward
+        self.viewUp = viewUp
+        self.rightDirection = vectProduct(viewForward, viewUp)
+        self.positions = positions
+
+        # Register arguments to the data handler
+        self.dataHandler.registerArgument(
+            priority=0, name="orientation", values=["f", "b", "r", "l", "u", "d"]
+        )
+
+        # Register arguments to id position
+        self.args = {}
+        for pos in positions:
+            for key in pos["args"]:
+                if key not in self.args:
+                    self.args[key] = {}
+                self.args[key][pos["args"][key]] = True
+
+        for key in self.args:
+            self.args[key] = sorted(self.args[key], key=lambda k: int(k))
+
+        self.keyList = self.args.keys()
+        for key in self.args:
+            self.dataHandler.registerArgument(
+                priority=1, name=key, values=self.args[key]
+            )
+
+        self.dataHandler.updateBasePattern()
+
+    def updatePriority(self, priorityList):
+        keyList = ["orientation"]
+        for idx in range(min(len(priorityList), len(keyList))):
+            self.dataHandler.updatePriority(keyList[idx], priorityList[idx])
+
+    def __iter__(self):
+        for pos in self.positions:
+            cameraData = {
+                "position": pos["position"],
+            }
+
+            print("=" * 80)
+            for key in pos["args"]:
+                idx = self.args[key].index(pos["args"][key])
+                self.dataHandler.setArguments(**{key: idx})
+                print(key, idx)
+
+            print("position", cameraData["position"])
+
+            # front
+            cameraData["focalPoint"] = [
+                (cameraData["position"][i] + self.viewForward[i]) for i in range(3)
+            ]
+            cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
+            cameraData["orientation"] = "front"
+            self.dataHandler.setArguments(orientation=0)
+            yield cameraData
+
+            # back
+            cameraData["focalPoint"] = [
+                (cameraData["position"][i] - self.viewForward[i]) for i in range(3)
+            ]
+            cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
+            cameraData["orientation"] = "back"
+            self.dataHandler.setArguments(orientation=1)
+            yield cameraData
+
+            # right
+            self.dataHandler.setArguments(orientation=2)
+            cameraData["focalPoint"] = [
+                (cameraData["position"][i] + self.rightDirection[i]) for i in range(3)
+            ]
+            cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
+            cameraData["orientation"] = "right"
+            yield cameraData
+
+            # left
+            self.dataHandler.setArguments(orientation=3)
+            cameraData["focalPoint"] = [
+                (cameraData["position"][i] - self.rightDirection[i]) for i in range(3)
+            ]
+            cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
+            cameraData["orientation"] = "left"
+            yield cameraData
+
+            # up
+            self.dataHandler.setArguments(orientation=4)
+            cameraData["focalPoint"] = [
+                (cameraData["position"][i] + self.viewUp[i]) for i in range(3)
+            ]
+            cameraData["viewUp"] = [(-self.viewForward[i]) for i in range(3)]
+            cameraData["orientation"] = "up"
+            yield cameraData
+
+            # doww
+            self.dataHandler.setArguments(orientation=5)
+            cameraData["focalPoint"] = [
+                (cameraData["position"][i] - self.viewUp[i]) for i in range(3)
+            ]
+            cameraData["viewUp"] = [self.viewForward[i] for i in range(3)]
+            cameraData["orientation"] = "down"
+            yield cameraData
+
+
+# -----------------------------------------------------------------------------
+# MultiView Cube Camera
+# -----------------------------------------------------------------------------
+
+
+class StereoCubeCamera(object):
+
+    # positions = [ { position: [x,y,z], args: { i: 1, j: 0, k: 7 } }, ... ]
+    def __init__(self, dataHandler, viewForward, viewUp, positions, eyeSpacing):
+        self.dataHandler = dataHandler
+        self.cameraSettings = []
+        self.viewForward = viewForward
+        self.viewUp = viewUp
+        self.rightDirection = vectProduct(viewForward, viewUp)
+        self.positions = positions
+        self.eyeSpacing = eyeSpacing
+
+        # Register arguments to the data handler
+        self.dataHandler.registerArgument(
+            priority=0, name="orientation", values=["f", "b", "r", "l", "u", "d"]
+        )
+        self.dataHandler.registerArgument(
+            priority=0, name="eye", values=["left", "right"]
+        )
+
+        # Register arguments to id position
+        self.args = {}
+        for pos in positions:
+            for key in pos["args"]:
+                if key not in self.args:
+                    self.args[key] = {}
+                self.args[key][pos["args"][key]] = True
+
+        for key in self.args:
+            self.args[key] = sorted(self.args[key], key=lambda k: int(k))
+
+        self.keyList = self.args.keys()
+        for key in self.args:
+            self.dataHandler.registerArgument(
+                priority=1, name=key, values=self.args[key]
+            )
+
+        self.dataHandler.updateBasePattern()
+
+    def updatePriority(self, priorityList):
+        keyList = ["orientation"]
+        for idx in range(min(len(priorityList), len(keyList))):
+            self.dataHandler.updatePriority(keyList[idx], priorityList[idx])
+
+    def __iter__(self):
+        for pos in self.positions:
+            cameraData = {}
+
+            for key in pos["args"]:
+                idx = self.args[key].index(pos["args"][key])
+                self.dataHandler.setArguments(**{key: idx})
+
+            # front
+            cameraData["orientation"] = "front"
+            self.dataHandler.setArguments(orientation=0)
+            deltaVect = [
+                (v * float(self.eyeSpacing) * 0.5) for v in self.rightDirection
+            ]
+            ## Left-Eye
+            self.dataHandler.setArguments(eye=0)
+            cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
+            cameraData["position"] = [
+                (pos["position"][idx] - deltaVect[idx]) for idx in range(3)
+            ]
+            cameraData["focalPoint"] = [
+                (pos["position"][i] + self.viewForward[i] - deltaVect[i])
+                for i in range(3)
+            ]
+            yield cameraData
+            ## Right-Eye
+            self.dataHandler.setArguments(eye=1)
+            cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
+            cameraData["position"] = [
+                (pos["position"][idx] + deltaVect[idx]) for idx in range(3)
+            ]
+            cameraData["focalPoint"] = [
+                (pos["position"][i] + self.viewForward[i] + deltaVect[i])
+                for i in range(3)
+            ]
+            yield cameraData
+
+            # back
+            cameraData["orientation"] = "back"
+            self.dataHandler.setArguments(orientation=1)
+            deltaVect = [
+                -(v * float(self.eyeSpacing) * 0.5) for v in self.rightDirection
+            ]
+            ## Left-Eye
+            self.dataHandler.setArguments(eye=0)
+            cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
+            cameraData["position"] = [
+                (pos["position"][idx] - deltaVect[idx]) for idx in range(3)
+            ]
+            cameraData["focalPoint"] = [
+                (pos["position"][i] - self.viewForward[i] - deltaVect[i])
+                for i in range(3)
+            ]
+            yield cameraData
+            ## Right-Eye
+            self.dataHandler.setArguments(eye=1)
+            cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
+            cameraData["position"] = [
+                (pos["position"][idx] + deltaVect[idx]) for idx in range(3)
+            ]
+            cameraData["focalPoint"] = [
+                (pos["position"][i] - self.viewForward[i] + deltaVect[i])
+                for i in range(3)
+            ]
+            yield cameraData
+
+            # right
+            self.dataHandler.setArguments(orientation=2)
+            cameraData["orientation"] = "right"
+            deltaVect = [-(v * float(self.eyeSpacing) * 0.5) for v in self.viewForward]
+            ## Left-Eye
+            self.dataHandler.setArguments(eye=0)
+            cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
+            cameraData["position"] = [
+                (pos["position"][idx] - deltaVect[idx]) for idx in range(3)
+            ]
+            cameraData["focalPoint"] = [
+                (pos["position"][i] + self.rightDirection[i] - deltaVect[i])
+                for i in range(3)
+            ]
+            yield cameraData
+            ## Right-Eye
+            self.dataHandler.setArguments(eye=1)
+            cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
+            cameraData["position"] = [
+                (pos["position"][idx] + deltaVect[idx]) for idx in range(3)
+            ]
+            cameraData["focalPoint"] = [
+                (pos["position"][i] + self.rightDirection[i] + deltaVect[i])
+                for i in range(3)
+            ]
+            yield cameraData
+
+            # left
+            self.dataHandler.setArguments(orientation=3)
+            cameraData["orientation"] = "left"
+            deltaVect = [(v * float(self.eyeSpacing) * 0.5) for v in self.viewForward]
+            ## Left-Eye
+            self.dataHandler.setArguments(eye=0)
+            cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
+            cameraData["position"] = [
+                (pos["position"][idx] - deltaVect[idx]) for idx in range(3)
+            ]
+            cameraData["focalPoint"] = [
+                (pos["position"][i] - self.rightDirection[i] - deltaVect[i])
+                for i in range(3)
+            ]
+            yield cameraData
+            ## Right-Eye
+            self.dataHandler.setArguments(eye=1)
+            cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
+            cameraData["position"] = [
+                (pos["position"][idx] + deltaVect[idx]) for idx in range(3)
+            ]
+            cameraData["focalPoint"] = [
+                (pos["position"][i] - self.rightDirection[i] + deltaVect[i])
+                for i in range(3)
+            ]
+            yield cameraData
+
+            # up
+            self.dataHandler.setArguments(orientation=4)
+            cameraData["orientation"] = "up"
+            deltaVect = [
+                (v * float(self.eyeSpacing) * 0.5) for v in self.rightDirection
+            ]
+            ## Left-Eye
+            self.dataHandler.setArguments(eye=0)
+            cameraData["viewUp"] = [(-self.viewForward[i]) for i in range(3)]
+            cameraData["position"] = [
+                (pos["position"][idx] - deltaVect[idx]) for idx in range(3)
+            ]
+            cameraData["focalPoint"] = [
+                (pos["position"][i] + self.viewUp[i] - deltaVect[i]) for i in range(3)
+            ]
+            yield cameraData
+            ## Right-Eye
+            self.dataHandler.setArguments(eye=1)
+            cameraData["viewUp"] = [(-self.viewForward[i]) for i in range(3)]
+            cameraData["position"] = [
+                (pos["position"][idx] + deltaVect[idx]) for idx in range(3)
+            ]
+            cameraData["focalPoint"] = [
+                (pos["position"][i] + self.viewUp[i] + deltaVect[i]) for i in range(3)
+            ]
+            yield cameraData
+
+            # doww
+            self.dataHandler.setArguments(orientation=5)
+            cameraData["orientation"] = "down"
+            deltaVect = [
+                (v * float(self.eyeSpacing) * 0.5) for v in self.rightDirection
+            ]
+            ## Left-Eye
+            self.dataHandler.setArguments(eye=0)
+            cameraData["viewUp"] = [self.viewForward[i] for i in range(3)]
+            cameraData["position"] = [
+                (pos["position"][idx] - deltaVect[idx]) for idx in range(3)
+            ]
+            cameraData["focalPoint"] = [
+                (pos["position"][i] - self.viewUp[i] - deltaVect[i]) for i in range(3)
+            ]
+            yield cameraData
+            ## Right-Eye
+            self.dataHandler.setArguments(eye=1)
+            cameraData["viewUp"] = [self.viewForward[i] for i in range(3)]
+            cameraData["position"] = [
+                (pos["position"][idx] + deltaVect[idx]) for idx in range(3)
+            ]
+            cameraData["focalPoint"] = [
+                (pos["position"][i] - self.viewUp[i] + deltaVect[i]) for i in range(3)
+            ]
+            yield cameraData
+
+
+# -----------------------------------------------------------------------------
+# MultiView Camera
+# -----------------------------------------------------------------------------
+
+
+class MultiViewCamera(object):
+    def __init__(self, dataHandler):
+        self.dataHandler = dataHandler
+        self.cameraSettings = []
+        self.positionNames = []
+
+    def registerViewPoint(self, name, focalPoint, position, viewUp):
+        self.cameraSettings.append(
+            {
+                "name": name,
+                "nameIdx": len(self.positionNames),
+                "focalPoint": focalPoint,
+                "position": position,
+                "viewUp": viewUp,
+            }
+        )
+        self.positionNames.append(name)
+        self.dataHandler.registerArgument(
+            priority=0, name="multiView", values=self.positionNames
+        )
+        self.dataHandler.updateBasePattern()
+
+    def updatePriority(self, priorityList):
+        keyList = ["multiView"]
+        for idx in range(min(len(priorityList), len(keyList))):
+            self.dataHandler.updatePriority(keyList[idx], priorityList[idx])
+
+    def __iter__(self):
+        for cameraData in self.cameraSettings:
+            self.dataHandler.setArguments(multiView=cameraData["nameIdx"])
+            yield cameraData
+
+
+# -----------------------------------------------------------------------------
+# Helper methods
+# -----------------------------------------------------------------------------
+
+
+def update_camera(renderer, cameraData):
+    camera = renderer.GetActiveCamera()
+    camera.SetPosition(cameraData["position"])
+    camera.SetFocalPoint(cameraData["focalPoint"])
+    camera.SetViewUp(cameraData["viewUp"])
+
+
+def create_spherical_camera(renderer, dataHandler, phiValues, thetaValues):
+    camera = renderer.GetActiveCamera()
+    return SphericalCamera(
+        dataHandler,
+        camera.GetFocalPoint(),
+        camera.GetPosition(),
+        camera.GetViewUp(),
+        phiValues,
+        thetaValues,
+    )
+
+
+def create_cylindrical_camera(renderer, dataHandler, phiValues, translationValues):
+    camera = renderer.GetActiveCamera()
+    return CylindricalCamera(
+        dataHandler,
+        camera.GetFocalPoint(),
+        camera.GetPosition(),
+        camera.GetViewUp(),
+        phiValues,
+        translationValues,
+    )
diff --git a/Web/Python/vtkmodules/web/dataset_builder.py b/Web/Python/vtkmodules/web/dataset_builder.py
new file mode 100644 (file)
index 0000000..5a997b1
--- /dev/null
@@ -0,0 +1,620 @@
+import json, os, gzip, shutil
+
+from vtkmodules.vtkRenderingCore import vtkWindowToImageFilter
+from vtkmodules.vtkIOImage import vtkPNGReader, vtkPNGWriter, vtkJPEGWriter
+from vtkmodules.vtkCommonDataModel import vtkImageData
+from vtkmodules.vtkCommonCore import vtkUnsignedCharArray
+from vtkmodules.vtkFiltersParallel import vtkPResampleFilter
+
+from vtkmodules.web import iteritems, getJSArrayType
+from vtkmodules.web.camera import (
+    update_camera,
+    create_spherical_camera,
+    create_cylindrical_camera,
+)
+from vtkmodules.web.query_data_model import DataHandler
+
+# Global helper variables
+encode_codes = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+
+# -----------------------------------------------------------------------------
+# Capture image from render window
+# -----------------------------------------------------------------------------
+
+
+class CaptureRenderWindow(object):
+    def __init__(self, magnification=1):
+        self.windowToImage = vtkWindowToImageFilter()
+        self.windowToImage.SetScale(magnification)
+        self.windowToImage.SetInputBufferTypeToRGB()
+        self.windowToImage.ReadFrontBufferOn()
+        self.writer = None
+
+    def SetRenderWindow(self, renderWindow):
+        self.windowToImage.SetInput(renderWindow)
+
+    def SetFormat(self, mimeType):
+        if mimeType == "image/png":
+            self.writer = vtkPNGWriter()
+            self.writer.SetInputConnection(self.windowToImage.GetOutputPort())
+        elif mimeType == "image/jpg":
+            self.writer = vtkJPEGWriter()
+            self.writer.SetInputConnection(self.windowToImage.GetOutputPort())
+
+    def writeImage(self, path):
+        if self.writer:
+            self.windowToImage.Modified()
+            self.windowToImage.Update()
+            self.writer.SetFileName(path)
+            self.writer.Write()
+
+
+# -----------------------------------------------------------------------------
+# Basic Dataset Builder
+# -----------------------------------------------------------------------------
+
+
+class DataSetBuilder(object):
+    def __init__(self, location, camera_data, metadata={}, sections={}):
+        self.dataHandler = DataHandler(location)
+        self.cameraDescription = camera_data
+        self.camera = None
+        self.imageCapture = CaptureRenderWindow()
+
+        for key, value in iteritems(metadata):
+            self.dataHandler.addMetaData(key, value)
+
+        for key, value in iteritems(sections):
+            self.dataHandler.addSection(key, value)
+
+    def getDataHandler(self):
+        return self.dataHandler
+
+    def getCamera(self):
+        return self.camera
+
+    def updateCamera(self, camera):
+        update_camera(self.renderer, camera)
+        self.renderWindow.Render()
+
+    def start(self, renderWindow=None, renderer=None):
+        if renderWindow:
+            # Keep track of renderWindow and renderer
+            self.renderWindow = renderWindow
+            self.renderer = renderer
+
+            # Initialize image capture
+            self.imageCapture.SetRenderWindow(renderWindow)
+
+            # Handle camera if any
+            if self.cameraDescription:
+                if self.cameraDescription["type"] == "spherical":
+                    self.camera = create_spherical_camera(
+                        renderer,
+                        self.dataHandler,
+                        self.cameraDescription["phi"],
+                        self.cameraDescription["theta"],
+                    )
+                elif self.cameraDescription["type"] == "cylindrical":
+                    self.camera = create_cylindrical_camera(
+                        renderer,
+                        self.dataHandler,
+                        self.cameraDescription["phi"],
+                        self.cameraDescription["translation"],
+                    )
+
+            # Update background color
+            bgColor = renderer.GetBackground()
+            bgColorString = "rgb(%d, %d, %d)" % tuple(
+                int(bgColor[i] * 255) for i in range(3)
+            )
+            self.dataHandler.addMetaData("backgroundColor", bgColorString)
+
+        # Update file patterns
+        self.dataHandler.updateBasePattern()
+
+    def stop(self):
+        self.dataHandler.writeDataDescriptor()
+
+
+# -----------------------------------------------------------------------------
+# Image Dataset Builder
+# -----------------------------------------------------------------------------
+
+
+class ImageDataSetBuilder(DataSetBuilder):
+    def __init__(self, location, imageMimeType, cameraInfo, metadata={}, sections={}):
+        DataSetBuilder.__init__(self, location, cameraInfo, metadata, sections)
+        imageExtenstion = "." + imageMimeType.split("/")[1]
+        self.dataHandler.registerData(
+            name="image", type="blob", mimeType=imageMimeType, fileName=imageExtenstion
+        )
+        self.imageCapture.SetFormat(imageMimeType)
+
+    def writeImage(self):
+        self.imageCapture.writeImage(self.dataHandler.getDataAbsoluteFilePath("image"))
+
+    def writeImages(self):
+        for cam in self.camera:
+            update_camera(self.renderer, cam)
+            self.renderWindow.Render()
+            self.imageCapture.writeImage(
+                self.dataHandler.getDataAbsoluteFilePath("image")
+            )
+
+
+# -----------------------------------------------------------------------------
+# Volume Composite Dataset Builder
+# -----------------------------------------------------------------------------
+class VolumeCompositeDataSetBuilder(DataSetBuilder):
+    def __init__(self, location, imageMimeType, cameraInfo, metadata={}, sections={}):
+        DataSetBuilder.__init__(self, location, cameraInfo, metadata, sections)
+
+        self.dataHandler.addTypes("volume-composite", "rgba+depth")
+
+        self.imageMimeType = imageMimeType
+        self.imageExtenstion = "." + imageMimeType.split("/")[1]
+
+        if imageMimeType == "image/png":
+            self.imageWriter = vtkPNGWriter()
+        if imageMimeType == "image/jpg":
+            self.imageWriter = vtkJPEGWriter()
+
+        self.imageDataColor = vtkImageData()
+        self.imageWriter.SetInputData(self.imageDataColor)
+
+        self.imageDataDepth = vtkImageData()
+        self.depthToWrite = None
+
+        self.layerInfo = {}
+        self.colorByMapping = {}
+        self.compositePipeline = {
+            "layers": [],
+            "dimensions": [],
+            "fields": {},
+            "layer_fields": {},
+            "pipeline": [],
+        }
+        self.activeDepthKey = ""
+        self.activeRGBKey = ""
+        self.nodeWithChildren = {}
+
+    def _getColorCode(self, colorBy):
+        if colorBy in self.colorByMapping:
+            # The color code exist
+            return self.colorByMapping[colorBy]
+        else:
+            # No color code assigned yet
+            colorCode = encode_codes[len(self.colorByMapping)]
+            # Assign color code
+            self.colorByMapping[colorBy] = colorCode
+            # Register color code with color by value
+            self.compositePipeline["fields"][colorCode] = colorBy
+            # Return the color code
+            return colorCode
+
+    def _getLayerCode(self, parent, layerName):
+        if layerName in self.layerInfo:
+            # Layer already exist
+            return (self.layerInfo[layerName]["code"], False)
+        else:
+            layerCode = encode_codes[len(self.layerInfo)]
+            self.layerInfo[layerName] = {
+                "code": layerCode,
+                "name": layerName,
+                "parent": parent,
+            }
+            self.compositePipeline["layers"].append(layerCode)
+            self.compositePipeline["layer_fields"][layerCode] = []
+
+            # Let's register it in the pipeline
+            if parent:
+                if parent not in self.nodeWithChildren:
+                    # Need to create parent
+                    rootNode = {"name": parent, "ids": [], "children": []}
+                    self.nodeWithChildren[parent] = rootNode
+                    self.compositePipeline["pipeline"].append(rootNode)
+
+                # Add node to its parent
+                self.nodeWithChildren[parent]["children"].append(
+                    {"name": layerName, "ids": [layerCode]}
+                )
+                self.nodeWithChildren[parent]["ids"].append(layerCode)
+
+            else:
+                self.compositePipeline["pipeline"].append(
+                    {"name": layerName, "ids": [layerCode]}
+                )
+
+            return (layerCode, True)
+
+    def _needToRegisterColor(self, layerCode, colorCode):
+        if colorCode in self.compositePipeline["layer_fields"][layerCode]:
+            return False
+        else:
+            self.compositePipeline["layer_fields"][layerCode].append(colorCode)
+            return True
+
+    def activateLayer(self, parent, name, colorBy):
+        layerCode, needToRegisterDepth = self._getLayerCode(parent, name)
+        colorCode = self._getColorCode(colorBy)
+        needToRegisterColor = self._needToRegisterColor(layerCode, colorCode)
+
+        # Update active keys
+        self.activeDepthKey = "%s_depth" % layerCode
+        self.activeRGBKey = "%s%s_rgb" % (layerCode, colorCode)
+
+        # Need to register data
+        if needToRegisterDepth:
+            self.dataHandler.registerData(
+                name=self.activeDepthKey,
+                type="array",
+                fileName="/%s_depth.uint8" % layerCode,
+                categories=[layerCode],
+            )
+
+        if needToRegisterColor:
+            self.dataHandler.registerData(
+                name=self.activeRGBKey,
+                type="blob",
+                fileName="/%s%s_rgb%s" % (layerCode, colorCode, self.imageExtenstion),
+                categories=["%s%s" % (layerCode, colorCode)],
+                mimeType=self.imageMimeType,
+            )
+
+    def writeData(self, mapper):
+        width = self.renderWindow.GetSize()[0]
+        height = self.renderWindow.GetSize()[1]
+
+        if not self.depthToWrite:
+            self.depthToWrite = bytearray(width * height)
+
+        for cam in self.camera:
+            self.updateCamera(cam)
+            imagePath = self.dataHandler.getDataAbsoluteFilePath(self.activeRGBKey)
+            depthPath = self.dataHandler.getDataAbsoluteFilePath(self.activeDepthKey)
+
+            # -----------------------------------------------------------------
+            # Write Image
+            # -----------------------------------------------------------------
+            mapper.GetColorImage(self.imageDataColor)
+            self.imageWriter.SetFileName(imagePath)
+            self.imageWriter.Write()
+
+            # -----------------------------------------------------------------
+            # Write Depth
+            # -----------------------------------------------------------------
+            mapper.GetDepthImage(self.imageDataDepth)
+            inputArray = self.imageDataDepth.GetPointData().GetArray(0)
+            size = inputArray.GetNumberOfTuples()
+            for idx in range(size):
+                self.depthToWrite[idx] = int(inputArray.GetValue(idx))
+
+            with open(depthPath, "wb") as f:
+                f.write(self.depthToWrite)
+
+    def start(self, renderWindow, renderer):
+        DataSetBuilder.start(self, renderWindow, renderer)
+        self.camera.updatePriority([2, 1])
+
+    def stop(self, compress=True):
+        # Push metadata
+        self.compositePipeline["dimensions"] = self.renderWindow.GetSize()
+        self.compositePipeline["default_pipeline"] = (
+            "A".join(self.compositePipeline["layers"]) + "A"
+        )
+        self.dataHandler.addSection("CompositePipeline", self.compositePipeline)
+
+        # Write metadata
+        DataSetBuilder.stop(self)
+
+        if compress:
+            for root, dirs, files in os.walk(self.dataHandler.getBasePath()):
+                print("Compress", root)
+                for name in files:
+                    if ".uint8" in name and ".gz" not in name:
+                        with open(os.path.join(root, name), "rb") as f_in:
+                            with gzip.open(
+                                os.path.join(root, name + ".gz"), "wb"
+                            ) as f_out:
+                                shutil.copyfileobj(f_in, f_out)
+                        os.remove(os.path.join(root, name))
+
+
+# -----------------------------------------------------------------------------
+# Data Prober Dataset Builder
+# -----------------------------------------------------------------------------
+class DataProberDataSetBuilder(DataSetBuilder):
+    def __init__(
+        self,
+        location,
+        sampling_dimesions,
+        fields_to_keep,
+        custom_probing_bounds=None,
+        metadata={},
+    ):
+        DataSetBuilder.__init__(self, location, None, metadata)
+        self.fieldsToWrite = fields_to_keep
+        self.resamplerFilter = vtkPResampleFilter()
+        self.resamplerFilter.SetSamplingDimension(sampling_dimesions)
+        if custom_probing_bounds:
+            self.resamplerFilter.SetUseInputBounds(0)
+            self.resamplerFilter.SetCustomSamplingBounds(custom_probing_bounds)
+        else:
+            self.resamplerFilter.SetUseInputBounds(1)
+
+        # Register all fields
+        self.dataHandler.addTypes("data-prober", "binary")
+        self.DataProber = {
+            "types": {},
+            "dimensions": sampling_dimesions,
+            "ranges": {},
+            "spacing": [1, 1, 1],
+        }
+        for field in self.fieldsToWrite:
+            self.dataHandler.registerData(
+                name=field, type="array", fileName="/%s.array" % field
+            )
+
+    def setDataToProbe(self, dataset):
+        self.resamplerFilter.SetInputData(dataset)
+
+    def setSourceToProbe(self, source):
+        self.resamplerFilter.SetInputConnection(source.GetOutputPort())
+
+    def writeData(self):
+        self.resamplerFilter.Update()
+        arrays = self.resamplerFilter.GetOutput().GetPointData()
+        for field in self.fieldsToWrite:
+            array = arrays.GetArray(field)
+            if array:
+                b = memoryview(array)
+                with open(self.dataHandler.getDataAbsoluteFilePath(field), "wb") as f:
+                    f.write(b)
+
+                self.DataProber["types"][field] = getJSArrayType(array)
+                if field in self.DataProber["ranges"]:
+                    dataRange = array.GetRange()
+                    if dataRange[0] < self.DataProber["ranges"][field][0]:
+                        self.DataProber["ranges"][field][0] = dataRange[0]
+                    if dataRange[1] > self.DataProber["ranges"][field][1]:
+                        self.DataProber["ranges"][field][1] = dataRange[1]
+                else:
+                    self.DataProber["ranges"][field] = [
+                        array.GetRange()[0],
+                        array.GetRange()[1],
+                    ]
+            else:
+                print("No array for", field)
+                print(self.resamplerFilter.GetOutput())
+
+    def stop(self, compress=True):
+        # Push metadata
+        self.dataHandler.addSection("DataProber", self.DataProber)
+
+        # Write metadata
+        DataSetBuilder.stop(self)
+
+        if compress:
+            for root, dirs, files in os.walk(self.dataHandler.getBasePath()):
+                print("Compress", root)
+                for name in files:
+                    if ".array" in name and ".gz" not in name:
+                        with open(os.path.join(root, name), "rb") as f_in:
+                            with gzip.open(
+                                os.path.join(root, name + ".gz"), "wb"
+                            ) as f_out:
+                                shutil.copyfileobj(f_in, f_out)
+                        os.remove(os.path.join(root, name))
+
+
+# -----------------------------------------------------------------------------
+# Sorted Composite Dataset Builder
+# -----------------------------------------------------------------------------
+class ConvertVolumeStackToSortedStack(object):
+    def __init__(self, width, height):
+        self.width = width
+        self.height = height
+        self.layers = 0
+
+    def convert(self, directory):
+        imagePaths = {}
+        depthPaths = {}
+        layerNames = []
+        for fileName in os.listdir(directory):
+            if "_rgb" in fileName or "_depth" in fileName:
+                fileId = fileName.split("_")[0][0]
+                if "_rgb" in fileName:
+                    imagePaths[fileId] = os.path.join(directory, fileName)
+                else:
+                    layerNames.append(fileId)
+                    depthPaths[fileId] = os.path.join(directory, fileName)
+
+        layerNames.sort()
+
+        if len(layerNames) == 0:
+            return
+
+        # Load data in Memory
+        depthArrays = []
+        imageReader = vtkPNGReader()
+        numberOfValues = self.width * self.height * len(layerNames)
+        imageSize = self.width * self.height
+        self.layers = len(layerNames)
+
+        # Write all images as single memoryview
+        opacity = vtkUnsignedCharArray()
+        opacity.SetNumberOfComponents(1)
+        opacity.SetNumberOfTuples(numberOfValues)
+
+        intensity = vtkUnsignedCharArray()
+        intensity.SetNumberOfComponents(1)
+        intensity.SetNumberOfTuples(numberOfValues)
+
+        for layer in range(self.layers):
+            imageReader.SetFileName(imagePaths[layerNames[layer]])
+            imageReader.Update()
+
+            rgbaArray = imageReader.GetOutput().GetPointData().GetArray(0)
+
+            for idx in range(imageSize):
+                intensity.SetValue(
+                    (layer * imageSize) + idx, rgbaArray.GetValue(idx * 4)
+                )
+                opacity.SetValue(
+                    (layer * imageSize) + idx, rgbaArray.GetValue(idx * 4 + 3)
+                )
+
+            with open(depthPaths[layerNames[layer]], "rb") as depthFile:
+                depthArrays.append(depthFile.read())
+
+        # Apply pixel sorting
+        destOrder = vtkUnsignedCharArray()
+        destOrder.SetNumberOfComponents(1)
+        destOrder.SetNumberOfTuples(numberOfValues)
+
+        opacityOrder = vtkUnsignedCharArray()
+        opacityOrder.SetNumberOfComponents(1)
+        opacityOrder.SetNumberOfTuples(numberOfValues)
+
+        intensityOrder = vtkUnsignedCharArray()
+        intensityOrder.SetNumberOfComponents(1)
+        intensityOrder.SetNumberOfTuples(numberOfValues)
+
+        for pixelIdx in range(imageSize):
+            depthStack = []
+            for depthArray in depthArrays:
+                depthStack.append((depthArray[pixelIdx], len(depthStack)))
+            depthStack.sort(key=lambda tup: tup[0])
+
+            for destLayerIdx in range(len(depthStack)):
+                sourceLayerIdx = depthStack[destLayerIdx][1]
+
+                # Copy Idx
+                destOrder.SetValue(
+                    (imageSize * destLayerIdx) + pixelIdx, sourceLayerIdx
+                )
+                opacityOrder.SetValue(
+                    (imageSize * destLayerIdx) + pixelIdx,
+                    opacity.GetValue((imageSize * sourceLayerIdx) + pixelIdx),
+                )
+                intensityOrder.SetValue(
+                    (imageSize * destLayerIdx) + pixelIdx,
+                    intensity.GetValue((imageSize * sourceLayerIdx) + pixelIdx),
+                )
+
+        with open(os.path.join(directory, "alpha.uint8"), "wb") as f:
+            f.write(memoryview(opacityOrder))
+
+        with open(os.path.join(directory, "intensity.uint8"), "wb") as f:
+            f.write(memoryview(intensityOrder))
+
+        with open(os.path.join(directory, "order.uint8"), "wb") as f:
+            f.write(memoryview(destOrder))
+
+
+class SortedCompositeDataSetBuilder(VolumeCompositeDataSetBuilder):
+    def __init__(self, location, cameraInfo, metadata={}, sections={}):
+        VolumeCompositeDataSetBuilder.__init__(
+            self, location, "image/png", cameraInfo, metadata, sections
+        )
+        self.dataHandler.addTypes("sorted-composite", "rgba")
+
+        # Register order and color textures
+        self.layerScalars = []
+        self.dataHandler.registerData(
+            name="order", type="array", fileName="/order.uint8"
+        )
+        self.dataHandler.registerData(
+            name="alpha", type="array", fileName="/alpha.uint8"
+        )
+        self.dataHandler.registerData(
+            name="intensity",
+            type="array",
+            fileName="/intensity.uint8",
+            categories=["intensity"],
+        )
+
+    def start(self, renderWindow, renderer):
+        VolumeCompositeDataSetBuilder.start(self, renderWindow, renderer)
+        imageSize = self.renderWindow.GetSize()
+        self.dataConverter = ConvertVolumeStackToSortedStack(imageSize[0], imageSize[1])
+
+    def activateLayer(self, colorBy, scalar):
+        VolumeCompositeDataSetBuilder.activateLayer(
+            self, "root", "%s" % scalar, colorBy
+        )
+        self.layerScalars.append(scalar)
+
+    def writeData(self, mapper):
+        VolumeCompositeDataSetBuilder.writeData(self, mapper)
+
+        # Fill data pattern
+        self.dataHandler.getDataAbsoluteFilePath("order")
+        self.dataHandler.getDataAbsoluteFilePath("alpha")
+        self.dataHandler.getDataAbsoluteFilePath("intensity")
+
+    def stop(self, clean=True, compress=True):
+        VolumeCompositeDataSetBuilder.stop(self, compress=False)
+
+        # Go through all directories and convert them
+        for root, dirs, files in os.walk(self.dataHandler.getBasePath()):
+            for name in dirs:
+                print("Process", os.path.join(root, name))
+                self.dataConverter.convert(os.path.join(root, name))
+
+        # Rename index.json to info_origin.json
+        os.rename(
+            os.path.join(self.dataHandler.getBasePath(), "index.json"),
+            os.path.join(self.dataHandler.getBasePath(), "index_origin.json"),
+        )
+
+        # Update index.json
+        with open(
+            os.path.join(self.dataHandler.getBasePath(), "index_origin.json"), "r"
+        ) as infoFile:
+            metadata = json.load(infoFile)
+            metadata["SortedComposite"] = {
+                "dimensions": metadata["CompositePipeline"]["dimensions"],
+                "layers": self.dataConverter.layers,
+                "scalars": self.layerScalars[0 : self.dataConverter.layers],
+            }
+
+            # Clean metadata
+            dataToKeep = []
+            del metadata["CompositePipeline"]
+            for item in metadata["data"]:
+                if item["name"] in ["order", "alpha", "intensity"]:
+                    dataToKeep.append(item)
+            metadata["data"] = dataToKeep
+            metadata["type"] = ["tonic-query-data-model", "sorted-composite", "alpha"]
+
+            # Override index.json
+            with open(
+                os.path.join(self.dataHandler.getBasePath(), "index.json"), "w"
+            ) as newMetaFile:
+                newMetaFile.write(json.dumps(metadata))
+
+        # Clean temporary data
+        if clean:
+            for root, dirs, files in os.walk(self.dataHandler.getBasePath()):
+                print("Clean", root)
+                for name in files:
+                    if (
+                        "_rgb.png" in name
+                        or "_depth.uint8" in name
+                        or name == "index_origin.json"
+                    ):
+                        os.remove(os.path.join(root, name))
+
+        if compress:
+            for root, dirs, files in os.walk(self.dataHandler.getBasePath()):
+                print("Compress", root)
+                for name in files:
+                    if ".uint8" in name and ".gz" not in name:
+                        with open(os.path.join(root, name), "rb") as f_in:
+                            with gzip.open(
+                                os.path.join(root, name + ".gz"), "wb"
+                            ) as f_out:
+                                shutil.copyfileobj(f_in, f_out)
+                        os.remove(os.path.join(root, name))
diff --git a/Web/Python/vtkmodules/web/errors.py b/Web/Python/vtkmodules/web/errors.py
new file mode 100644 (file)
index 0000000..3d8e442
--- /dev/null
@@ -0,0 +1,12 @@
+WEB_DEPENDENCY_MISSING_MESSAGE = """Please install VTK's Web module dependencies.
+
+These include `wslink` and can be easily installed  with vtk by using the
+`web` extra requirements option. For example:
+
+    pip install vtk[web]
+
+"""
+
+class WebDependencyMissingError(ImportError):
+    def __init__(self, message=WEB_DEPENDENCY_MISSING_MESSAGE):
+        super().__init__(message)
diff --git a/Web/Python/vtkmodules/web/protocols.py b/Web/Python/vtkmodules/web/protocols.py
new file mode 100644 (file)
index 0000000..312bb31
--- /dev/null
@@ -0,0 +1,842 @@
+r"""protocols is a module that contains a set of VTK Web related
+protocols that can be combined together to provide a flexible way to define
+very specific web application.
+"""
+
+from __future__ import absolute_import, division, print_function
+
+import os, sys, logging, types, inspect, traceback, re, base64, time
+
+from vtkmodules.vtkWebCore import vtkWebInteractionEvent
+
+from vtkmodules.web.errors import WebDependencyMissingError
+from vtkmodules.web.render_window_serializer import (
+    serializeInstance,
+    SynchronizationContext,
+    getReferenceId,
+    initializeSerializers,
+)
+
+try:
+    from wslink import schedule_callback
+    from wslink import register as exportRpc
+    from wslink.websocket import LinkProtocol
+except ImportError:
+    raise WebDependencyMissingError()
+
+# =============================================================================
+#
+# Base class for any VTK Web based protocol
+#
+# =============================================================================
+
+
+class vtkWebProtocol(LinkProtocol):
+    def getApplication(self):
+        return self.getSharedObject("app")
+
+    # no need for a setApplication anymore, but keep for compatibility
+    def setApplication(self, app):
+        pass
+
+    def mapIdToObject(self, id):
+        """
+        Maps global-id for a vtkObject to the vtkObject instance. May return None if the
+        id is not valid.
+        """
+        id = int(id)
+        if id <= 0:
+            return None
+        return self.getApplication().GetObjectIdMap().GetVTKObject(id)
+
+    def getGlobalId(self, obj):
+        """
+        Return the id for a given vtkObject
+        """
+        return self.getApplication().GetObjectIdMap().GetGlobalId(obj)
+
+    def freeObject(self, obj):
+        """
+        Delete the given vtkObject from the objectIdMap. Returns true if delete succeeded.
+        """
+        return self.getApplication().GetObjectIdMap().FreeObject(obj)
+
+    def freeObjectById(self, id):
+        """
+        Delete the vtkObject corresponding to the given objectId from the objectIdMap.
+        Returns true if delete succeeded.
+        """
+        return self.getApplication().GetObjectIdMap().FreeObjectById(id)
+
+    def getView(self, vid):
+        """
+        Returns the view for a given view ID, if vid is None then return the
+        current active view.
+        :param vid: The view ID
+        :type vid: str
+        """
+        v = self.mapIdToObject(vid)
+
+        if not v:
+            # Use active view is none provided.
+            v = self.getApplication().GetObjectIdMap().GetActiveObject("VIEW")
+        if not v:
+            raise Exception("no view provided: %s" % vid)
+
+        return v
+
+    def setActiveView(self, view):
+        """
+        Set a vtkRenderWindow to be the active one
+        """
+        self.getApplication().GetObjectIdMap().SetActiveObject("VIEW", view)
+
+
+# =============================================================================
+#
+# Handle Mouse interaction on any type of view
+#
+# =============================================================================
+
+
+class vtkWebMouseHandler(vtkWebProtocol):
+    @exportRpc("viewport.mouse.interaction")
+    def mouseInteraction(self, event):
+        """
+        RPC Callback for mouse interactions.
+        """
+        view = self.getView(event["view"])
+
+        buttons = 0
+        if event["buttonLeft"]:
+            buttons |= vtkWebInteractionEvent.LEFT_BUTTON
+        if event["buttonMiddle"]:
+            buttons |= vtkWebInteractionEvent.MIDDLE_BUTTON
+        if event["buttonRight"]:
+            buttons |= vtkWebInteractionEvent.RIGHT_BUTTON
+
+        modifiers = 0
+        if event["shiftKey"]:
+            modifiers |= vtkWebInteractionEvent.SHIFT_KEY
+        if event["ctrlKey"]:
+            modifiers |= vtkWebInteractionEvent.CTRL_KEY
+        if event["altKey"]:
+            modifiers |= vtkWebInteractionEvent.ALT_KEY
+        if event["metaKey"]:
+            modifiers |= vtkWebInteractionEvent.META_KEY
+
+        pvevent = vtkWebInteractionEvent()
+        pvevent.SetButtons(buttons)
+        pvevent.SetModifiers(modifiers)
+        if "x" in event:
+            pvevent.SetX(event["x"])
+        if "y" in event:
+            pvevent.SetY(event["y"])
+        if "scroll" in event:
+            pvevent.SetScroll(event["scroll"])
+        if event["action"] == "dblclick":
+            pvevent.SetRepeatCount(2)
+        # pvevent.SetKeyCode(event["charCode"])
+        retVal = self.getApplication().HandleInteractionEvent(view, pvevent)
+        del pvevent
+
+        if event["action"] == "down":
+            self.getApplication().InvokeEvent("StartInteractionEvent")
+
+        if event["action"] == "up":
+            self.getApplication().InvokeEvent("EndInteractionEvent")
+
+        if retVal:
+            self.getApplication().InvokeEvent("UpdateEvent")
+
+        return retVal
+
+    @exportRpc("viewport.mouse.zoom.wheel")
+    def updateZoomFromWheel(self, event):
+        if "Start" in event["type"]:
+            self.getApplication().InvokeEvent("StartInteractionEvent")
+
+        renderWindow = self.getView(event["view"])
+        if renderWindow and "spinY" in event:
+            zoomFactor = 1.0 - event["spinY"] / 10.0
+
+            camera = renderWindow.GetRenderers().GetFirstRenderer().GetActiveCamera()
+            fp = camera.GetFocalPoint()
+            pos = camera.GetPosition()
+            delta = [fp[i] - pos[i] for i in range(3)]
+            camera.Zoom(zoomFactor)
+
+            pos2 = camera.GetPosition()
+            camera.SetFocalPoint([pos2[i] + delta[i] for i in range(3)])
+            renderWindow.Modified()
+
+        if "End" in event["type"]:
+            self.getApplication().InvokeEvent("EndInteractionEvent")
+
+
+# =============================================================================
+#
+# Basic 3D Viewport API (Camera + Orientation + CenterOfRotation
+#
+# =============================================================================
+
+
+class vtkWebViewPort(vtkWebProtocol):
+    @exportRpc("viewport.camera.reset")
+    def resetCamera(self, viewId):
+        """
+        RPC callback to reset camera.
+        """
+        view = self.getView(viewId)
+        renderer = view.GetRenderers().GetFirstRenderer()
+        renderer.ResetCamera()
+
+        self.getApplication().InvalidateCache(view)
+        self.getApplication().InvokeEvent("UpdateEvent")
+
+        return str(self.getGlobalId(view))
+
+    @exportRpc("viewport.axes.orientation.visibility.update")
+    def updateOrientationAxesVisibility(self, viewId, showAxis):
+        """
+        RPC callback to show/hide OrientationAxis.
+        """
+        view = self.getView(viewId)
+        # FIXME seb: view.OrientationAxesVisibility = (showAxis if 1 else 0);
+
+        self.getApplication().InvalidateCache(view)
+        self.getApplication().InvokeEvent("UpdateEvent")
+
+        return str(self.getGlobalId(view))
+
+    @exportRpc("viewport.axes.center.visibility.update")
+    def updateCenterAxesVisibility(self, viewId, showAxis):
+        """
+        RPC callback to show/hide CenterAxesVisibility.
+        """
+        view = self.getView(viewId)
+        # FIXME seb: view.CenterAxesVisibility = (showAxis if 1 else 0);
+
+        self.getApplication().InvalidateCache(view)
+        self.getApplication().InvokeEvent("UpdateEvent")
+
+        return str(self.getGlobalId(view))
+
+    @exportRpc("viewport.camera.update")
+    def updateCamera(self, view_id, focal_point, view_up, position, forceUpdate=True):
+        view = self.getView(view_id)
+
+        camera = view.GetRenderers().GetFirstRenderer().GetActiveCamera()
+        camera.SetFocalPoint(focal_point)
+        camera.SetViewUp(view_up)
+        camera.SetPosition(position)
+
+        if forceUpdate:
+            self.getApplication().InvalidateCache(view)
+            self.getApplication().InvokeEvent("UpdateEvent")
+
+
+# =============================================================================
+#
+# Provide Image delivery mechanism (deprecated - will be removed in VTK 10+)
+#
+# =============================================================================
+
+
+class vtkWebViewPortImageDelivery(vtkWebProtocol):
+    @exportRpc("viewport.image.render")
+    def stillRender(self, options):
+        """
+        RPC Callback to render a view and obtain the rendered image.
+        """
+        beginTime = int(round(time.time() * 1000))
+        view = self.getView(options["view"])
+        size = [view.GetSize()[0], view.GetSize()[1]]
+        # use existing size, overridden only if options["size"] is set.
+        resize = size != options.get("size", size)
+        if resize:
+            size = options["size"]
+            if size[0] > 0 and size[1] > 0:
+                view.SetSize(size)
+        t = 0
+        if options and "mtime" in options:
+            t = options["mtime"]
+        quality = 100
+        if options and "quality" in options:
+            quality = options["quality"]
+        localTime = 0
+        if options and "localTime" in options:
+            localTime = options["localTime"]
+        reply = {}
+        app = self.getApplication()
+        if t == 0:
+            app.InvalidateCache(view)
+        reply["image"] = app.StillRenderToString(view, t, quality)
+        # Check that we are getting image size we have set. If not, wait until we
+        # do. The render call will set the actual window size.
+        tries = 10
+        while resize and list(view.GetSize()) != size and size != [0, 0] and tries > 0:
+            app.InvalidateCache(view)
+            reply["image"] = app.StillRenderToString(view, t, quality)
+            tries -= 1
+
+        reply["stale"] = app.GetHasImagesBeingProcessed(view)
+        reply["mtime"] = app.GetLastStillRenderToMTime()
+        reply["size"] = [view.GetSize()[0], view.GetSize()[1]]
+        reply["format"] = "jpeg;base64"
+        reply["global_id"] = str(self.getGlobalId(view))
+        reply["localTime"] = localTime
+
+        endTime = int(round(time.time() * 1000))
+        reply["workTime"] = endTime - beginTime
+
+        return reply
+
+
+# =============================================================================
+#
+# Provide publish-based Image delivery mechanism
+#
+# =============================================================================
+
+
+class vtkWebPublishImageDelivery(vtkWebProtocol):
+    def __init__(self, decode=True):
+        super(vtkWebPublishImageDelivery, self).__init__()
+        self.trackingViews = {}
+        self.lastStaleTime = 0
+        self.staleHandlerCount = 0
+        self.deltaStaleTimeBeforeRender = 0.5  # 0.5s
+        self.decode = decode
+        self.viewsInAnimations = []
+        self.targetFrameRate = 30.0
+        self.minFrameRate = 12.0
+        self.maxFrameRate = 30.0
+
+    def pushRender(self, vId, ignoreAnimation=False):
+        if vId not in self.trackingViews:
+            return
+
+        if not self.trackingViews[vId]["enabled"]:
+            return
+
+        if not ignoreAnimation and len(self.viewsInAnimations) > 0:
+            return
+
+        if "originalSize" not in self.trackingViews[vId]:
+            view = self.getView(vId)
+            self.trackingViews[vId]["originalSize"] = list(view.GetSize())
+
+        if "ratio" not in self.trackingViews[vId]:
+            self.trackingViews[vId]["ratio"] = 1
+
+        ratio = self.trackingViews[vId]["ratio"]
+        mtime = self.trackingViews[vId]["mtime"]
+        quality = self.trackingViews[vId]["quality"]
+        size = [int(s * ratio) for s in self.trackingViews[vId]["originalSize"]]
+
+        reply = self.stillRender(
+            {"view": vId, "mtime": mtime, "quality": quality, "size": size}
+        )
+        stale = reply["stale"]
+        if reply["image"]:
+            # depending on whether the app has encoding enabled:
+            if self.decode:
+                reply["image"] = base64.standard_b64decode(reply["image"])
+
+            reply["image"] = self.addAttachment(reply["image"])
+            reply["format"] = "jpeg"
+            # save mtime for next call.
+            self.trackingViews[vId]["mtime"] = reply["mtime"]
+            # echo back real ID, instead of -1 for 'active'
+            reply["id"] = vId
+            self.publish("viewport.image.push.subscription", reply)
+        if stale:
+            self.lastStaleTime = time.time()
+            if self.staleHandlerCount == 0:
+                self.staleHandlerCount += 1
+                schedule_callback(
+                    self.deltaStaleTimeBeforeRender, lambda: self.renderStaleImage(vId)
+                )
+        else:
+            self.lastStaleTime = 0
+
+    def renderStaleImage(self, vId):
+        self.staleHandlerCount -= 1
+
+        if self.lastStaleTime != 0:
+            delta = time.time() - self.lastStaleTime
+            if delta >= self.deltaStaleTimeBeforeRender:
+                self.pushRender(vId)
+            else:
+                self.staleHandlerCount += 1
+                schedule_callback(
+                    self.deltaStaleTimeBeforeRender - delta + 0.001,
+                    lambda: self.renderStaleImage(vId),
+                )
+
+    def animate(self):
+        if len(self.viewsInAnimations) == 0:
+            return
+
+        nextAnimateTime = time.time() + 1.0 / self.targetFrameRate
+        for vId in self.viewsInAnimations:
+            self.pushRender(vId, True)
+
+        nextAnimateTime -= time.time()
+
+        if self.targetFrameRate > self.maxFrameRate:
+            self.targetFrameRate = self.maxFrameRate
+
+        if nextAnimateTime < 0:
+            if nextAnimateTime < -1.0:
+                self.targetFrameRate = 1
+            if self.targetFrameRate > self.minFrameRate:
+                self.targetFrameRate -= 1.0
+            schedule_callback(0.001, lambda: self.animate())
+        else:
+            if self.targetFrameRate < self.maxFrameRate and nextAnimateTime > 0.005:
+                self.targetFrameRate += 1.0
+            schedule_callback(nextAnimateTime, lambda: self.animate())
+
+    @exportRpc("viewport.image.animation.fps.max")
+    def setMaxFrameRate(self, fps=30):
+        self.maxFrameRate = fps
+
+    @exportRpc("viewport.image.animation.fps.get")
+    def getCurrentFrameRate(self):
+        return self.targetFrameRate
+
+    @exportRpc("viewport.image.animation.start")
+    def startViewAnimation(self, viewId="-1"):
+        sView = self.getView(viewId)
+        realViewId = str(self.getGlobalId(sView))
+
+        self.viewsInAnimations.append(realViewId)
+        if len(self.viewsInAnimations) == 1:
+            self.animate()
+
+    @exportRpc("viewport.image.animation.stop")
+    def stopViewAnimation(self, viewId="-1"):
+        sView = self.getView(viewId)
+        realViewId = str(self.getGlobalId(sView))
+
+        if realViewId in self.viewsInAnimations:
+            self.viewsInAnimations.remove(realViewId)
+
+    @exportRpc("viewport.image.push")
+    def imagePush(self, options):
+        sView = self.getView(options["view"])
+        realViewId = str(self.getGlobalId(sView))
+        # Make sure an image is pushed
+        self.getApplication().InvalidateCache(sView)
+        self.pushRender(realViewId)
+
+    # Internal function since the reply[image] is not
+    # JSON(serializable) it can not be an RPC one
+    def stillRender(self, options):
+        """
+        RPC Callback to render a view and obtain the rendered image.
+        """
+        beginTime = int(round(time.time() * 1000))
+        view = self.getView(options["view"])
+        size = view.GetSize()[0:2]
+        resize = size != options.get("size", size)
+        if resize:
+            size = options["size"]
+            if size[0] > 10 and size[1] > 10:
+                view.SetSize(size)
+        t = 0
+        if options and "mtime" in options:
+            t = options["mtime"]
+        quality = 100
+        if options and "quality" in options:
+            quality = options["quality"]
+        localTime = 0
+        if options and "localTime" in options:
+            localTime = options["localTime"]
+        reply = {}
+        app = self.getApplication()
+        if t == 0:
+            app.InvalidateCache(view)
+        if self.decode:
+            stillRender = app.StillRenderToString
+        else:
+            stillRender = app.StillRenderToBuffer
+        reply_image = stillRender(view, t, quality)
+
+        # Check that we are getting image size we have set if not wait until we
+        # do. The render call will set the actual window size.
+        tries = 10
+        while resize and list(view.GetSize()) != size and size != [0, 0] and tries > 0:
+            app.InvalidateCache(view)
+            reply_image = stillRender(view, t, quality)
+            tries -= 1
+
+        if (
+            not resize
+            and options
+            and ("clearCache" in options)
+            and options["clearCache"]
+        ):
+            app.InvalidateCache(view)
+            reply_image = stillRender(view, t, quality)
+
+        reply["stale"] = app.GetHasImagesBeingProcessed(view)
+        reply["mtime"] = app.GetLastStillRenderToMTime()
+        reply["size"] = view.GetSize()[0:2]
+        reply["memsize"] = reply_image.GetDataSize() if reply_image else 0
+        reply["format"] = "jpeg;base64" if self.decode else "jpeg"
+        reply["global_id"] = str(self.getGlobalId(view))
+        reply["localTime"] = localTime
+        if self.decode:
+            reply["image"] = reply_image
+        else:
+            # Convert the vtkUnsignedCharArray into a bytes object, required by Autobahn websockets
+            reply["image"] = memoryview(reply_image).tobytes() if reply_image else None
+
+        endTime = int(round(time.time() * 1000))
+        reply["workTime"] = endTime - beginTime
+
+        return reply
+
+    @exportRpc("viewport.image.push.observer.add")
+    def addRenderObserver(self, viewId):
+        sView = self.getView(viewId)
+        if not sView:
+            return {"error": "Unable to get view with id %s" % viewId}
+
+        realViewId = str(self.getGlobalId(sView))
+
+        if not realViewId in self.trackingViews:
+            observerCallback = lambda *args, **kwargs: self.pushRender(realViewId)
+            startCallback = lambda *args, **kwargs: self.startViewAnimation(realViewId)
+            stopCallback = lambda *args, **kwargs: self.stopViewAnimation(realViewId)
+            tag = self.getApplication().AddObserver("UpdateEvent", observerCallback)
+            tagStart = self.getApplication().AddObserver(
+                "StartInteractionEvent", startCallback
+            )
+            tagStop = self.getApplication().AddObserver(
+                "EndInteractionEvent", stopCallback
+            )
+            # TODO do we need self.getApplication().AddObserver('ResetActiveView', resetActiveView())
+            self.trackingViews[realViewId] = {
+                "tags": [tag, tagStart, tagStop],
+                "observerCount": 1,
+                "mtime": 0,
+                "enabled": True,
+                "quality": 100,
+            }
+        else:
+            # There is an observer on this view already
+            self.trackingViews[realViewId]["observerCount"] += 1
+
+        self.pushRender(realViewId)
+        return {"success": True, "viewId": realViewId}
+
+    @exportRpc("viewport.image.push.observer.remove")
+    def removeRenderObserver(self, viewId):
+        sView = self.getView(viewId)
+        if not sView:
+            return {"error": "Unable to get view with id %s" % viewId}
+
+        realViewId = str(self.getGlobalId(sView))
+
+        observerInfo = None
+        if realViewId in self.trackingViews:
+            observerInfo = self.trackingViews[realViewId]
+
+        if not observerInfo:
+            return {"error": "Unable to find subscription for view %s" % realViewId}
+
+        observerInfo["observerCount"] -= 1
+
+        if observerInfo["observerCount"] <= 0:
+            for tag in observerInfo["tags"]:
+                self.getApplication().RemoveObserver(tag)
+            del self.trackingViews[realViewId]
+
+        return {"result": "success"}
+
+    @exportRpc("viewport.image.push.quality")
+    def setViewQuality(self, viewId, quality, ratio=1):
+        sView = self.getView(viewId)
+        if not sView:
+            return {"error": "Unable to get view with id %s" % viewId}
+
+        realViewId = str(self.getGlobalId(sView))
+        observerInfo = None
+        if realViewId in self.trackingViews:
+            observerInfo = self.trackingViews[realViewId]
+
+        if not observerInfo:
+            return {"error": "Unable to find subscription for view %s" % realViewId}
+
+        observerInfo["quality"] = quality
+        observerInfo["ratio"] = ratio
+
+        # Update image size right now!
+        if "originalSize" in self.trackingViews[realViewId]:
+            size = [
+                int(s * ratio) for s in self.trackingViews[realViewId]["originalSize"]
+            ]
+            if hasattr(sView, "SetSize"):
+                sView.SetSize(size)
+            else:
+                sView.ViewSize = size
+
+        return {"result": "success"}
+
+    @exportRpc("viewport.image.push.original.size")
+    def setViewSize(self, viewId, width, height):
+        sView = self.getView(viewId)
+        if not sView:
+            return {"error": "Unable to get view with id %s" % viewId}
+
+        realViewId = str(self.getGlobalId(sView))
+        observerInfo = None
+        if realViewId in self.trackingViews:
+            observerInfo = self.trackingViews[realViewId]
+
+        if not observerInfo:
+            return {"error": "Unable to find subscription for view %s" % realViewId}
+
+        observerInfo["originalSize"] = [width, height]
+
+        return {"result": "success"}
+
+    @exportRpc("viewport.image.push.enabled")
+    def enableView(self, viewId, enabled):
+        sView = self.getView(viewId)
+        if not sView:
+            return {"error": "Unable to get view with id %s" % viewId}
+
+        realViewId = str(self.getGlobalId(sView))
+        observerInfo = None
+        if realViewId in self.trackingViews:
+            observerInfo = self.trackingViews[realViewId]
+
+        if not observerInfo:
+            return {"error": "Unable to find subscription for view %s" % realViewId}
+
+        observerInfo["enabled"] = enabled
+
+        return {"result": "success"}
+
+    @exportRpc("viewport.image.push.invalidate.cache")
+    def invalidateCache(self, viewId):
+        sView = self.getView(viewId)
+        if not sView:
+            return {"error": "Unable to get view with id %s" % viewId}
+
+        self.getApplication().InvalidateCache(sView)
+        self.getApplication().InvokeEvent("UpdateEvent")
+        return {"result": "success"}
+
+
+# =============================================================================
+#
+# Provide Geometry delivery mechanism (WebGL) (deprecated - will be removed in VTK 10+)
+#
+# =============================================================================
+
+
+class vtkWebViewPortGeometryDelivery(vtkWebProtocol):
+    @exportRpc("viewport.webgl.metadata")
+    def getSceneMetaData(self, view_id):
+        view = self.getView(view_id)
+        data = self.getApplication().GetWebGLSceneMetaData(view)
+        return data
+
+    @exportRpc("viewport.webgl.data")
+    def getWebGLData(self, view_id, object_id, part):
+        view = self.getView(view_id)
+        data = self.getApplication().GetWebGLBinaryData(view, str(object_id), part - 1)
+        return data
+
+
+# =============================================================================
+#
+# Provide File/Directory listing
+#
+# =============================================================================
+
+
+class vtkWebFileBrowser(vtkWebProtocol):
+    def __init__(
+        self, basePath, name, excludeRegex=r"^\.|~$|^\$", groupRegex=r"[0-9]+\."
+    ):
+        """
+        Configure the way the WebFile browser will expose the server content.
+         - basePath: specify the base directory that we should start with
+         - name: Name of that base directory that will show up on the web
+         - excludeRegex: Regular expression of what should be excluded from the list of files/directories
+        """
+        self.baseDirectory = basePath
+        self.rootName = name
+        self.pattern = re.compile(excludeRegex)
+        self.gPattern = re.compile(groupRegex)
+
+    @exportRpc("file.server.directory.list")
+    def listServerDirectory(self, relativeDir="."):
+        """
+        RPC Callback to list a server directory relative to the basePath
+        provided at start-up.
+        """
+        path = [self.rootName]
+        if len(relativeDir) > len(self.rootName):
+            relativeDir = relativeDir[len(self.rootName) + 1 :]
+            path += relativeDir.replace("\\", "/").split("/")
+
+        currentPath = os.path.join(self.baseDirectory, relativeDir)
+        result = {
+            "label": relativeDir,
+            "files": [],
+            "dirs": [],
+            "groups": [],
+            "path": path,
+        }
+        if relativeDir == ".":
+            result["label"] = self.rootName
+        for file in os.listdir(currentPath):
+            if os.path.isfile(os.path.join(currentPath, file)) and not re.search(
+                self.pattern, file
+            ):
+                result["files"].append({"label": file, "size": -1})
+            elif os.path.isdir(os.path.join(currentPath, file)) and not re.search(
+                self.pattern, file
+            ):
+                result["dirs"].append(file)
+
+        # Filter files to create groups
+        files = result["files"]
+        files.sort()
+        groups = result["groups"]
+        groupIdx = {}
+        filesToRemove = []
+        for file in files:
+            fileSplit = re.split(self.gPattern, file["label"])
+            if len(fileSplit) == 2:
+                filesToRemove.append(file)
+                gName = "*.".join(fileSplit)
+                if gName in groupIdx:
+                    groupIdx[gName]["files"].append(file["label"])
+                else:
+                    groupIdx[gName] = {"files": [file["label"]], "label": gName}
+                    groups.append(groupIdx[gName])
+        for file in filesToRemove:
+            gName = "*.".join(re.split(self.gPattern, file["label"]))
+            if len(groupIdx[gName]["files"]) > 1:
+                files.remove(file)
+            else:
+                groups.remove(groupIdx[gName])
+
+        return result
+
+
+# =============================================================================
+#
+# Provide an updated geometry delivery mechanism which better matches the
+# client-side rendering capability we have in vtk.js
+#
+# =============================================================================
+
+
+class vtkWebLocalRendering(vtkWebProtocol):
+    def __init__(self, **kwargs):
+        super(vtkWebLocalRendering, self).__init__()
+        initializeSerializers()
+        self.context = SynchronizationContext()
+        self.trackingViews = {}
+        self.mtime = 0
+
+    # RpcName: getArray => viewport.geometry.array.get
+    @exportRpc("viewport.geometry.array.get")
+    def getArray(self, dataHash, binary=False):
+        if binary:
+            return self.addAttachment(self.context.getCachedDataArray(dataHash, binary))
+        return self.context.getCachedDataArray(dataHash, binary)
+
+    # RpcName: addViewObserver => viewport.geometry.view.observer.add
+    @exportRpc("viewport.geometry.view.observer.add")
+    def addViewObserver(self, viewId):
+        sView = self.getView(viewId)
+        if not sView:
+            return {"error": "Unable to get view with id %s" % viewId}
+
+        realViewId = self.getApplication().GetObjectIdMap().GetGlobalId(sView)
+
+        def pushGeometry(newSubscription=False):
+            stateToReturn = self.getViewState(realViewId, newSubscription)
+            stateToReturn["mtime"] = 0 if newSubscription else self.mtime
+            self.mtime += 1
+            return stateToReturn
+
+        if not realViewId in self.trackingViews:
+            observerCallback = lambda *args, **kwargs: self.publish(
+                "viewport.geometry.view.subscription", pushGeometry()
+            )
+            tag = self.getApplication().AddObserver("UpdateEvent", observerCallback)
+            self.trackingViews[realViewId] = {"tags": [tag], "observerCount": 1}
+        else:
+            # There is an observer on this view already
+            self.trackingViews[realViewId]["observerCount"] += 1
+
+        self.publish("viewport.geometry.view.subscription", pushGeometry(True))
+        return {"success": True, "viewId": realViewId}
+
+    # RpcName: removeViewObserver => viewport.geometry.view.observer.remove
+    @exportRpc("viewport.geometry.view.observer.remove")
+    def removeViewObserver(self, viewId):
+        sView = self.getView(viewId)
+        if not sView:
+            return {"error": "Unable to get view with id %s" % viewId}
+
+        realViewId = self.getApplication().GetObjectIdMap().GetGlobalId(sView)
+
+        observerInfo = None
+        if realViewId in self.trackingViews:
+            observerInfo = self.trackingViews[realViewId]
+
+        if not observerInfo:
+            return {"error": "Unable to find subscription for view %s" % realViewId}
+
+        observerInfo["observerCount"] -= 1
+
+        if observerInfo["observerCount"] <= 0:
+            for tag in observerInfo["tags"]:
+                self.getApplication().RemoveObserver(tag)
+            del self.trackingViews[realViewId]
+
+        return {"result": "success"}
+
+    # RpcName: getViewState => viewport.geometry.view.get.state
+    @exportRpc("viewport.geometry.view.get.state")
+    def getViewState(self, viewId, newSubscription=False):
+        sView = self.getView(viewId)
+        if not sView:
+            return {"error": "Unable to get view with id %s" % viewId}
+
+        self.context.setIgnoreLastDependencies(newSubscription)
+
+        # Get the active view and render window, use it to iterate over renderers
+        renderWindow = sView
+        renderer = renderWindow.GetRenderers().GetFirstRenderer()
+        camera = renderer.GetActiveCamera()
+        renderWindowId = self.getApplication().GetObjectIdMap().GetGlobalId(sView)
+        viewInstance = serializeInstance(
+            None, renderWindow, renderWindowId, self.context, 1
+        )
+        viewInstance["extra"] = {
+            "vtkRefId": getReferenceId(renderWindow),
+            "centerOfRotation": camera.GetFocalPoint(),
+            "camera": getReferenceId(camera),
+        }
+
+        self.context.setIgnoreLastDependencies(False)
+        self.context.checkForArraysToRelease()
+
+        if viewInstance:
+            return viewInstance
+
+        return None
diff --git a/Web/Python/vtkmodules/web/query_data_model.py b/Web/Python/vtkmodules/web/query_data_model.py
new file mode 100644 (file)
index 0000000..0ac388f
--- /dev/null
@@ -0,0 +1,182 @@
+"""
+Core Module for Web Base Data Generation
+"""
+
+import sys, os, json
+
+from vtkmodules.web import iteritems
+
+
+class DataHandler(object):
+    def __init__(self, basePath):
+        self.__root = basePath
+        self.types = ["tonic-query-data-model"]
+        self.metadata = {}
+        self.data = {}
+        self.arguments = {}
+        self.current = {}
+        self.sections = {}
+        self.basePattern = None
+        self.priority = []
+        self.argOrder = []
+        self.realValues = {}
+        self.can_write = True
+
+    def getBasePath(self):
+        return self.__root
+
+    def updateBasePattern(self):
+        self.priority.sort(key=lambda item: item[1])
+        self.basePattern = ""
+        patternSeparator = ""
+        currentPriority = -1
+
+        for item in self.priority:
+            if currentPriority != -1:
+                if currentPriority == item[1]:
+                    patternSeparator = "_"
+                else:
+                    patternSeparator = "/"
+            currentPriority = item[1]
+            self.basePattern = "{%s}%s%s" % (
+                item[0],
+                patternSeparator,
+                self.basePattern,
+            )
+
+    def registerArgument(self, **kwargs):
+        """
+        We expect the following set of arguments
+         - priority
+         - name
+         - label (optional)
+         - values
+         - uiType
+         - defaultIdx
+        """
+        newArgument = {}
+        argName = kwargs["name"]
+        self.argOrder.append(argName)
+        for key, value in iteritems(kwargs):
+            if key == "priority":
+                self.priority.append([argName, value])
+            elif key == "values":
+                self.realValues[argName] = value
+                newArgument[key] = ["{value}".format(value=x) for x in value]
+            else:
+                newArgument[key] = value
+
+        self.arguments[argName] = newArgument
+
+    def updatePriority(self, argumentName, newPriority):
+        for item in self.priority:
+            if item[0] == argumentName:
+                item[1] = newPriority
+
+    def setArguments(self, **kwargs):
+        """
+        Update the arguments index
+        """
+        for key, value in iteritems(kwargs):
+            self.current[key] = value
+
+    def removeData(self, name):
+        del self.data[name]
+
+    def registerData(self, **kwargs):
+        """
+        name, type, mimeType, fileName, dependencies
+        """
+        newData = {"metadata": {}}
+        argName = kwargs["name"]
+        for key, value in iteritems(kwargs):
+            if key == "fileName":
+                if "rootFile" in kwargs and kwargs["rootFile"]:
+                    newData["pattern"] = "{pattern}/%s" % value
+                else:
+                    newData["pattern"] = "{pattern}%s" % value
+            else:
+                newData[key] = value
+
+        self.data[argName] = newData
+
+    def addDataMetaData(self, name, key, value):
+        self.data[name]["metadata"][key] = value
+
+    def getDataAbsoluteFilePath(self, name, createDirectories=True):
+        dataPattern = self.data[name]["pattern"]
+        if "{pattern}" in dataPattern:
+            if len(self.basePattern) == 0:
+                dataPattern = dataPattern.replace(
+                    "{pattern}/", self.basePattern
+                ).replace("{pattern}", self.basePattern)
+                self.data[name]["pattern"] = dataPattern
+            else:
+                dataPattern = dataPattern.replace("{pattern}", self.basePattern)
+                self.data[name]["pattern"] = dataPattern
+
+        keyValuePair = {}
+        for key, value in iteritems(self.current):
+            keyValuePair[key] = self.arguments[key]["values"][value]
+
+        fullpath = os.path.join(self.__root, dataPattern.format(**keyValuePair))
+
+        if createDirectories and self.can_write:
+            if not os.path.exists(os.path.dirname(fullpath)):
+                os.makedirs(os.path.dirname(fullpath))
+
+        return fullpath
+
+    def addTypes(self, *args):
+        for arg in args:
+            self.types.append(arg)
+
+    def addMetaData(self, key, value):
+        self.metadata[key] = value
+
+    def addSection(self, key, value):
+        self.sections[key] = value
+
+    def computeDataPatterns(self):
+        if self.basePattern == None:
+            self.updateBasePattern()
+
+        for name in self.data:
+            dataPattern = self.data[name]["pattern"]
+            if "{pattern}" in dataPattern:
+                dataPattern = dataPattern.replace("{pattern}", self.basePattern)
+                self.data[name]["pattern"] = dataPattern
+
+    def __getattr__(self, name):
+        if self.basePattern == None:
+            self.updateBasePattern()
+
+        for i in range(len(self.arguments[name]["values"])):
+            self.current[name] = i
+            yield self.realValues[name][i]
+
+    def writeDataDescriptor(self):
+        if not self.can_write:
+            return
+
+        self.computeDataPatterns()
+
+        jsonData = {
+            "arguments_order": self.argOrder,
+            "type": self.types,
+            "arguments": self.arguments,
+            "metadata": self.metadata,
+            "data": [],
+        }
+
+        # Add sections
+        for key, value in iteritems(self.sections):
+            jsonData[key] = value
+
+        # Add data
+        for key, value in iteritems(self.data):
+            jsonData["data"].append(value)
+
+        filePathToWrite = os.path.join(self.__root, "index.json")
+        with open(filePathToWrite, "w") as fileToWrite:
+            fileToWrite.write(json.dumps(jsonData))
diff --git a/Web/Python/vtkmodules/web/render_window_serializer.py b/Web/Python/vtkmodules/web/render_window_serializer.py
new file mode 100644 (file)
index 0000000..5431867
--- /dev/null
@@ -0,0 +1,1410 @@
+import io
+import logging
+import struct
+import time
+import zipfile
+
+from vtkmodules.web import (
+    base64Encode,
+    hashDataArray,
+    getJSArrayType,
+    arrayTypesMapping,
+    getReferenceId,
+)
+
+from vtkmodules.vtkCommonCore import vtkTypeUInt32Array
+from vtkmodules.vtkFiltersGeometry import vtkCompositeDataGeometryFilter
+from vtkmodules.vtkFiltersGeometry import vtkDataSetSurfaceFilter
+from vtkmodules.vtkRenderingCore import vtkColorTransferFunction
+
+logger = logging.getLogger(__name__)
+# Always DEBUG level for this logger. Users can change this
+logger.setLevel(logging.DEBUG)
+
+# -----------------------------------------------------------------------------
+# Array helpers
+# -----------------------------------------------------------------------------
+
+def zipCompression(name, data):
+    with io.BytesIO() as in_memory:
+        with zipfile.ZipFile(in_memory, mode="w") as zf:
+            zf.writestr("data/%s" % name, data, zipfile.ZIP_DEFLATED)
+        in_memory.seek(0)
+        return in_memory.read()
+
+
+def dataTableToList(dataTable):
+    dataType = arrayTypesMapping[dataTable.GetDataType()]
+    elementSize = struct.calcsize(dataType)
+    nbValues = dataTable.GetNumberOfValues()
+    nbComponents = dataTable.GetNumberOfComponents()
+    nbytes = elementSize * nbValues
+    if dataType != " ":
+        with io.BytesIO(memoryview(dataTable)) as stream:
+            data = list(struct.unpack(dataType * nbValues, stream.read(nbytes)))
+        return [
+            data[idx * nbComponents : (idx + 1) * nbComponents]
+            for idx in range(nbValues // nbComponents)
+        ]
+
+    return None
+
+
+# -----------------------------------------------------------------------------
+
+
+def linspace(start, stop, num):
+    delta = (stop - start) / (num - 1)
+    return [start + i * delta for i in range(num)]
+
+
+# -----------------------------------------------------------------------------
+# Convenience class for caching data arrays, storing computed sha sums, keeping
+# track of valid actors, etc...
+# -----------------------------------------------------------------------------
+
+
+class SynchronizationContext:
+    def __init__(self):
+        self.dataArrayCache = {}
+        self.lastDependenciesMapping = {}
+        self.ingoreLastDependencies = False
+
+    def setIgnoreLastDependencies(self, force):
+        self.ingoreLastDependencies = force
+
+    def cacheDataArray(self, pMd5, data):
+        self.dataArrayCache[pMd5] = data
+
+    def getCachedDataArray(self, pMd5, binary=False, compression=False):
+        cacheObj = self.dataArrayCache[pMd5]
+        array = cacheObj["array"]
+        cacheTime = cacheObj["mTime"]
+
+        if cacheTime != array.GetMTime():
+            logger.debug(" ***** ERROR: you asked for an old cache key! ***** ")
+
+        if array.GetDataType() == 12:
+            # IdType need to be converted to Uint32
+            arraySize = array.GetNumberOfTuples() * array.GetNumberOfComponents()
+            newArray = vtkTypeUInt32Array()
+            newArray.SetNumberOfTuples(arraySize)
+            for i in range(arraySize):
+                newArray.SetValue(i, -1 if array.GetValue(i) < 0 else array.GetValue(i))
+            pBuffer = memoryview(newArray)
+        else:
+            pBuffer = memoryview(array)
+
+        if binary:
+            # Convert the vtkUnsignedCharArray into a bytes object, required by
+            # Autobahn websockets
+            return (
+                pBuffer.tobytes()
+                if not compression
+                else zipCompression(pMd5, pBuffer.tobytes())
+            )
+
+        return base64Encode(
+            pBuffer if not compression else zipCompression(pMd5, pBuffer.tobytes())
+        )
+
+    def checkForArraysToRelease(self, timeWindow=20):
+        cutOffTime = time.time() - timeWindow
+        shasToDelete = []
+        for sha in self.dataArrayCache:
+            record = self.dataArrayCache[sha]
+            array = record["array"]
+            count = array.GetReferenceCount()
+
+            if count == 1 and record["ts"] < cutOffTime:
+                shasToDelete.append(sha)
+
+        for sha in shasToDelete:
+            del self.dataArrayCache[sha]
+
+    def getLastDependencyList(self, idstr):
+        lastDeps = []
+        if idstr in self.lastDependenciesMapping and not self.ingoreLastDependencies:
+            lastDeps = self.lastDependenciesMapping[idstr]
+        return lastDeps
+
+    def setNewDependencyList(self, idstr, depList):
+        self.lastDependenciesMapping[idstr] = depList
+
+    def buildDependencyCallList(self, idstr, newList, addMethod, removeMethod):
+        oldList = self.getLastDependencyList(idstr)
+
+        calls = []
+        calls += [[addMethod, [wrapId(x)]] for x in newList if x not in oldList]
+        calls += [[removeMethod, [wrapId(x)]] for x in oldList if x not in newList]
+
+        self.setNewDependencyList(idstr, newList)
+        return calls
+
+
+# -----------------------------------------------------------------------------
+# Global variables
+# -----------------------------------------------------------------------------
+
+SERIALIZERS = {}
+JS_CLASS_MAPPING = {}
+context = None
+
+# -----------------------------------------------------------------------------
+# Global API
+# -----------------------------------------------------------------------------
+
+
+def registerInstanceSerializer(name, method):
+    global SERIALIZERS
+    SERIALIZERS[name] = method
+
+def registerJSClass(vtk_class, js_class):
+    global JS_CLASS_MAPPING
+    JS_CLASS_MAPPING[vtk_class] = js_class
+
+def class_name(vtk_obj):
+    vtk_class = vtk_obj.GetClassName()
+    if vtk_class in JS_CLASS_MAPPING:
+        return JS_CLASS_MAPPING[vtk_class]
+
+    return vtk_class
+
+
+# -----------------------------------------------------------------------------
+
+
+def serializeInstance(parent, instance, instanceId, context, depth):
+    instanceType = class_name(instance)
+    serializer = SERIALIZERS[instanceType] if instanceType in SERIALIZERS else None
+
+    if serializer:
+        return serializer(parent, instance, instanceId, context, depth)
+
+    logger.error(f"!!!No serializer for {instanceType} with id {instanceId}")
+
+    return None
+
+
+# -----------------------------------------------------------------------------
+
+
+def initializeSerializers():
+    # Actors/viewProps
+    registerInstanceSerializer("vtkActor", genericActorSerializer)
+    registerInstanceSerializer("vtkOpenGLActor", genericActorSerializer)
+    registerInstanceSerializer("vtkPVLODActor", genericActorSerializer)
+
+    # Volume/viewProps
+    registerInstanceSerializer("vtkVolume", genericVolumeSerializer)
+
+    # Mappers
+    registerInstanceSerializer("vtkMapper", genericMapperSerializer)
+    registerInstanceSerializer("vtkDataSetMapper", genericMapperSerializer)
+    registerInstanceSerializer("vtkPolyDataMapper", genericMapperSerializer)
+    registerInstanceSerializer("vtkImageDataMapper", genericMapperSerializer)
+    registerInstanceSerializer("vtkOpenGLPolyDataMapper", genericMapperSerializer)
+    registerInstanceSerializer("vtkCompositePolyDataMapper2", genericMapperSerializer)
+    registerJSClass("vtkPolyDataMapper", "vtkMapper")
+    registerJSClass("vtkDataSetMapper", "vtkMapper")
+    registerJSClass("vtkOpenGLPolyDataMapper", "vtkMapper")
+    registerJSClass("vtkCompositePolyDataMapper2", "vtkMapper")
+
+    registerInstanceSerializer("vtkVolumeMapper", genericVolumeMapperSerializer)
+    registerInstanceSerializer("vtkFixedPointVolumeRayCastMapper", genericVolumeMapperSerializer)
+    registerJSClass("vtkFixedPointVolumeRayCastMapper", "vtkVolumeMapper")
+
+    # LookupTables/TransferFunctions
+    registerInstanceSerializer("vtkLookupTable", lookupTableSerializer2)
+    registerInstanceSerializer(
+        "vtkPVDiscretizableColorTransferFunction", discretizableColorTransferFunctionSerializer
+    )
+    registerInstanceSerializer(
+        "vtkColorTransferFunction", colorTransferFunctionSerializer
+    )
+    registerInstanceSerializer("vtkPiecewiseFunction", pwfSerializer)
+
+    # Textures
+    registerInstanceSerializer("vtkTexture", textureSerializer)
+    registerInstanceSerializer("vtkOpenGLTexture", textureSerializer)
+
+    # Property
+    registerInstanceSerializer("vtkProperty", propertySerializer)
+    registerInstanceSerializer("vtkOpenGLProperty", propertySerializer)
+
+    # VolumeProperty
+    registerInstanceSerializer("vtkVolumeProperty", volumePropertySerializer)
+
+    # Datasets
+    registerInstanceSerializer("vtkPolyData", polydataSerializer)
+    registerInstanceSerializer("vtkImageData", imagedataSerializer)
+    registerInstanceSerializer("vtkUnstructuredGrid", mergeToPolydataSerializer)
+    registerInstanceSerializer("vtkMultiBlockDataSet", mergeToPolydataSerializer)
+    registerInstanceSerializer("vtkStructuredPoints", imagedataSerializer)
+    registerJSClass("vtkStructuredPoints", "vtkImageData")
+
+
+    # RenderWindows
+    registerInstanceSerializer("vtkRenderWindow", renderWindowSerializer)
+    registerInstanceSerializer("vtkCocoaRenderWindow", renderWindowSerializer)
+    registerInstanceSerializer("vtkXOpenGLRenderWindow", renderWindowSerializer)
+    registerInstanceSerializer("vtkWin32OpenGLRenderWindow", renderWindowSerializer)
+    registerInstanceSerializer("vtkEGLRenderWindow", renderWindowSerializer)
+    registerInstanceSerializer("vtkOpenVRRenderWindow", renderWindowSerializer)
+    registerInstanceSerializer("vtkOpenXRRenderWindow", renderWindowSerializer)
+    registerInstanceSerializer("vtkGenericOpenGLRenderWindow", renderWindowSerializer)
+    registerInstanceSerializer("vtkOSOpenGLRenderWindow", renderWindowSerializer)
+    registerInstanceSerializer("vtkOpenGLRenderWindow", renderWindowSerializer)
+    registerInstanceSerializer("vtkIOSRenderWindow", renderWindowSerializer)
+    registerInstanceSerializer("vtkExternalOpenGLRenderWindow", renderWindowSerializer)
+    registerInstanceSerializer("vtkOffscreenOpenGLRenderWindow", renderWindowSerializer)
+
+    # Renderers
+    registerInstanceSerializer("vtkRenderer", rendererSerializer)
+    registerInstanceSerializer("vtkOpenGLRenderer", rendererSerializer)
+
+    # Cameras
+    registerInstanceSerializer("vtkCamera", cameraSerializer)
+    registerInstanceSerializer("vtkOpenGLCamera", cameraSerializer)
+
+    # Lights
+    registerInstanceSerializer("vtkLight", lightSerializer)
+    registerInstanceSerializer("vtkPVLight", lightSerializer)
+    registerInstanceSerializer("vtkOpenGLLight", lightSerializer)
+
+    # Annotations (ScalarBar/CubeAxes
+    registerInstanceSerializer("vtkCubeAxesActor", cubeAxesSerializer)
+    registerInstanceSerializer("vtkScalarBarActor", scalarBarActorSerializer)
+
+
+# -----------------------------------------------------------------------------
+# Helper functions
+# -----------------------------------------------------------------------------
+
+
+def pad(depth):
+    padding = ""
+    for _ in range(depth):
+        padding += "  "
+    return padding
+
+
+# -----------------------------------------------------------------------------
+
+
+def wrapId(idStr):
+    return "instance:${%s}" % idStr
+
+
+# -----------------------------------------------------------------------------
+
+dataArrayShaMapping = {}
+
+
+def digest(array):
+    objId = getReferenceId(array)
+
+    record = None
+    if objId in dataArrayShaMapping:
+        record = dataArrayShaMapping[objId]
+
+    if record and record["mtime"] == array.GetMTime():
+        return record["sha"]
+
+    record = {"sha": hashDataArray(array), "mtime": array.GetMTime()}
+
+    dataArrayShaMapping[objId] = record
+    return record["sha"]
+
+
+# -----------------------------------------------------------------------------
+
+
+def getRangeInfo(array, component):
+    r = array.GetRange(component)
+    compRange = {}
+    compRange["min"] = r[0]
+    compRange["max"] = r[1]
+    compRange["component"] = array.GetComponentName(component)
+    return compRange
+
+
+# -----------------------------------------------------------------------------
+
+
+def getArrayDescription(array, context):
+    if not array:
+        return None
+
+    pMd5 = digest(array)
+    context.cacheDataArray(
+        pMd5, {"array": array, "mTime": array.GetMTime(), "ts": time.time()}
+    )
+
+    root = {}
+    root["hash"] = pMd5
+    root["vtkClass"] = "vtkDataArray"
+    root["name"] = array.GetName()
+    root["dataType"] = getJSArrayType(array)
+    root["numberOfComponents"] = array.GetNumberOfComponents()
+    root["size"] = array.GetNumberOfComponents() * array.GetNumberOfTuples()
+    root["ranges"] = []
+    if root["numberOfComponents"] > 1:
+        for i in range(root["numberOfComponents"]):
+            root["ranges"].append(getRangeInfo(array, i))
+        root["ranges"].append(getRangeInfo(array, -1))
+    else:
+        root["ranges"].append(getRangeInfo(array, 0))
+
+    return root
+
+
+# -----------------------------------------------------------------------------
+
+
+def extractRequiredFields(
+    extractedFields, parent, dataset, context, requestedFields=["Normals", "TCoords"]
+):
+    arrays_to_export = set()
+    export_all = "*" in requestedFields
+    # Identify arrays to export
+    if not export_all:
+        # FIXME should evolve and support funky mapper which leverage many arrays
+        if parent and parent.IsA("vtkMapper"):
+            mapper = parent
+            scalarVisibility = mapper.GetScalarVisibility()
+            arrayAccessMode = mapper.GetArrayAccessMode()
+            colorArrayName = (
+                mapper.GetArrayName() if arrayAccessMode == 1 else mapper.GetArrayId()
+            )
+            # colorMode = mapper.GetColorMode()
+            scalarMode = mapper.GetScalarMode()
+            if scalarVisibility and scalarMode in (1, 3):
+                array_to_export = dataset.GetPointData().GetArray(colorArrayName)
+                if array_to_export is None:
+                    array_to_export = dataset.GetPointData().GetScalars()
+                arrays_to_export.add(array_to_export)
+            if scalarVisibility and scalarMode in (2, 4):
+                array_to_export = dataset.GetCellData().GetArray(colorArrayName)
+                if array_to_export is None:
+                    array_to_export = dataset.GetCellData().GetScalars()
+                arrays_to_export.add(array_to_export)
+            if scalarVisibility and scalarMode == 0:
+                array_to_export = dataset.GetPointData().GetScalars()
+                if array_to_export is None:
+                    array_to_export = dataset.GetCellData().GetScalars()
+                arrays_to_export.add(array_to_export)
+
+        if parent and parent.IsA("vtkTexture") and dataset.GetPointData().GetScalars():
+            arrays_to_export.add(dataset.GetPointData().GetScalars())
+
+        arrays_to_export.update(
+            [
+                getattr(dataset.GetPointData(), "Get" + requestedField, lambda: None)()
+                for requestedField in requestedFields
+            ]
+        )
+
+    # Browse all arrays
+    for location, field_data in [
+        ("pointData", dataset.GetPointData()),
+        ("cellData", dataset.GetCellData()),
+    ]:
+        for array_index in range(field_data.GetNumberOfArrays()):
+            array = field_data.GetArray(array_index)
+            if export_all or array in arrays_to_export:
+                arrayMeta = getArrayDescription(array, context)
+                if arrayMeta:
+                    arrayMeta["location"] = location
+                    attribute = field_data.IsArrayAnAttribute(array_index)
+                    arrayMeta["registration"] = (
+                        "set" + field_data.GetAttributeTypeAsString(attribute)
+                        if attribute >= 0
+                        else "addArray"
+                    )
+                    extractedFields.append(arrayMeta)
+
+# -----------------------------------------------------------------------------
+# Concrete instance serializers
+# -----------------------------------------------------------------------------
+
+
+def genericActorSerializer(parent, actor, actorId, context, depth):
+    # This kind of actor has two "children" of interest, a property and a
+    # mapper
+    actorVisibility = actor.GetVisibility()
+    mapperInstance = None
+    propertyInstance = None
+    calls = []
+    dependencies = []
+
+    if actorVisibility:
+        mapper = None
+        if not hasattr(actor, "GetMapper"):
+            logger.debug("This actor does not have a GetMapper method")
+        else:
+            mapper = actor.GetMapper()
+
+        if mapper:
+            mapperId = getReferenceId(mapper)
+            mapperInstance = serializeInstance(
+                actor, mapper, mapperId, context, depth + 1
+            )
+            if mapperInstance:
+                dependencies.append(mapperInstance)
+                calls.append(["setMapper", [wrapId(mapperId)]])
+
+        prop = None
+        if hasattr(actor, "GetProperty"):
+            prop = actor.GetProperty()
+        else:
+            logger.debug("This actor does not have a GetProperty method")
+
+        if prop:
+            propId = getReferenceId(prop)
+            propertyInstance = serializeInstance(
+                actor, prop, propId, context, depth + 1
+            )
+            if propertyInstance:
+                dependencies.append(propertyInstance)
+                calls.append(["setProperty", [wrapId(propId)]])
+
+        # Handle texture if any
+        texture = None
+        if hasattr(actor, "GetTexture"):
+            texture = actor.GetTexture()
+        else:
+            logger.debug("This actor does not have a GetTexture method")
+
+        if texture:
+            textureId = getReferenceId(texture)
+            textureInstance = serializeInstance(
+                actor, texture, textureId, context, depth + 1
+            )
+            if textureInstance:
+                dependencies.append(textureInstance)
+                calls.append(["addTexture", [wrapId(textureId)]])
+
+    if actorVisibility == 0 or (mapperInstance and propertyInstance):
+        return {
+            "parent": getReferenceId(parent),
+            "id": actorId,
+            "type": class_name(actor),
+            "properties": {
+                # vtkProp
+                "visibility": actorVisibility,
+                "pickable": actor.GetPickable(),
+                "dragable": actor.GetDragable(),
+                "useBounds": actor.GetUseBounds(),
+                # vtkProp3D
+                "origin": actor.GetOrigin(),
+                "position": actor.GetPosition(),
+                "scale": actor.GetScale(),
+                # vtkActor
+                "forceOpaque": actor.GetForceOpaque(),
+                "forceTranslucent": actor.GetForceTranslucent(),
+            },
+            "calls": calls,
+            "dependencies": dependencies,
+        }
+
+    return None
+
+
+# -----------------------------------------------------------------------------
+
+
+def genericVolumeSerializer(parent, actor, actorId, context, depth):
+    # This kind of actor has two "children" of interest, a property and a
+    # mapper
+    actorVisibility = actor.GetVisibility()
+    mapperInstance = None
+    propertyInstance = None
+    calls = []
+    dependencies = []
+
+    if actorVisibility:
+        mapper = None
+        if not hasattr(actor, "GetMapper"):
+            logger.debug("This actor does not have a GetMapper method")
+        else:
+            mapper = actor.GetMapper()
+
+        if mapper:
+            mapperId = getReferenceId(mapper)
+            mapperInstance = serializeInstance(
+                actor, mapper, mapperId, context, depth + 1
+            )
+            if mapperInstance:
+                dependencies.append(mapperInstance)
+                calls.append(["setMapper", [wrapId(mapperId)]])
+
+        prop = None
+        if hasattr(actor, "GetProperty"):
+            prop = actor.GetProperty()
+        else:
+            logger.debug("This actor does not have a GetProperty method")
+
+        if prop:
+            propId = getReferenceId(prop)
+            propertyInstance = serializeInstance(
+                actor, prop, propId, context, depth + 1
+            )
+            if propertyInstance:
+                dependencies.append(propertyInstance)
+                calls.append(["setProperty", [wrapId(propId)]])
+
+    if actorVisibility == 0 or (mapperInstance and propertyInstance):
+        return {
+            "parent": getReferenceId(parent),
+            "id": actorId,
+            "type": class_name(actor),
+            "properties": {
+                # vtkProp
+                "visibility": actorVisibility,
+                "pickable": actor.GetPickable(),
+                "dragable": actor.GetDragable(),
+                "useBounds": actor.GetUseBounds(),
+                # vtkProp3D
+                "origin": actor.GetOrigin(),
+                "position": actor.GetPosition(),
+                "scale": actor.GetScale(),
+            },
+            "calls": calls,
+            "dependencies": dependencies,
+        }
+
+    return None
+
+# -----------------------------------------------------------------------------
+
+
+def textureSerializer(parent, texture, textureId, context, depth):
+    # This kind of mapper requires us to get 2 items: input data and lookup
+    # table
+    dataObject = None
+    dataObjectInstance = None
+    calls = []
+    dependencies = []
+
+    if hasattr(texture, "GetInput"):
+        dataObject = texture.GetInput()
+    else:
+        logger.debug("This texture does not have GetInput method")
+
+    if dataObject:
+        dataObjectId = "%s-texture" % textureId
+        dataObjectInstance = serializeInstance(
+            texture, dataObject, dataObjectId, context, depth + 1
+        )
+        if dataObjectInstance:
+            dependencies.append(dataObjectInstance)
+            calls.append(["setInputData", [wrapId(dataObjectId)]])
+
+    if dataObjectInstance:
+        return {
+            "parent": getReferenceId(parent),
+            "id": textureId,
+            "type": "vtkTexture",
+            "properties": {
+                "interpolate": texture.GetInterpolate(),
+                "repeat": texture.GetRepeat(),
+                "edgeClamp": texture.GetEdgeClamp(),
+            },
+            "calls": calls,
+            "dependencies": dependencies,
+        }
+
+    return None
+
+
+# -----------------------------------------------------------------------------
+
+
+def genericMapperSerializer(parent, mapper, mapperId, context, depth):
+    # This kind of mapper requires us to get 2 items: input data and lookup
+    # table
+    dataObject = None
+    dataObjectInstance = None
+    lookupTableInstance = None
+    calls = []
+    dependencies = []
+
+    if hasattr(mapper, "GetInputDataObject"):
+        mapper.GetInputAlgorithm().Update()
+        dataObject = mapper.GetInputDataObject(0, 0)
+    else:
+        logger.debug("This mapper does not have GetInputDataObject method")
+
+    if dataObject:
+        if dataObject.IsA("vtkDataSet"):
+            alg = vtkDataSetSurfaceFilter()
+            alg.SetInputData(dataObject)
+            alg.Update()
+            dataObject = alg.GetOutput()
+
+        dataObjectId = "%s-dataset" % mapperId
+        dataObjectInstance = serializeInstance(
+            mapper, dataObject, dataObjectId, context, depth + 1
+        )
+
+        if dataObjectInstance:
+            dependencies.append(dataObjectInstance)
+            calls.append(["setInputData", [wrapId(dataObjectId)]])
+
+    lookupTable = None
+
+    if hasattr(mapper, "GetLookupTable"):
+        lookupTable = mapper.GetLookupTable()
+    else:
+        logger.debug("This mapper does not have GetLookupTable method")
+
+    if lookupTable:
+        lookupTableId = getReferenceId(lookupTable)
+        lookupTableInstance = serializeInstance(
+            mapper, lookupTable, lookupTableId, context, depth + 1
+        )
+        if lookupTableInstance:
+            dependencies.append(lookupTableInstance)
+            calls.append(
+                ["setLookupTable", [wrapId(lookupTableId)]]
+            )
+
+    if dataObjectInstance:
+        colorArrayName = (
+            mapper.GetArrayName()
+            if mapper.GetArrayAccessMode() == 1
+            else mapper.GetArrayId()
+        )
+        return {
+            "parent": getReferenceId(parent),
+            "id": mapperId,
+            "type": class_name(mapper),
+            "properties": {
+                "resolveCoincidentTopology": mapper.GetResolveCoincidentTopology(),
+                "renderTime": mapper.GetRenderTime(),
+                "arrayAccessMode": mapper.GetArrayAccessMode(),
+                "scalarRange": mapper.GetScalarRange(),
+                "useLookupTableScalarRange": 1
+                if mapper.GetUseLookupTableScalarRange()
+                else 0,
+                "scalarVisibility": mapper.GetScalarVisibility(),
+                "colorByArrayName": colorArrayName,
+                "colorMode": mapper.GetColorMode(),
+                "scalarMode": mapper.GetScalarMode(),
+                "interpolateScalarsBeforeMapping": 1
+                if mapper.GetInterpolateScalarsBeforeMapping()
+                else 0,
+            },
+            "calls": calls,
+            "dependencies": dependencies,
+        }
+
+    return None
+
+
+# -----------------------------------------------------------------------------
+
+
+def genericVolumeMapperSerializer(parent, mapper, mapperId, context, depth):
+    # This kind of mapper requires us to get 2 items: input data and lookup
+    # table
+    dataObject = None
+    dataObjectInstance = None
+    lookupTableInstance = None
+    calls = []
+    dependencies = []
+
+    if hasattr(mapper, "GetInputDataObject"):
+        mapper.GetInputAlgorithm().Update()
+        dataObject = mapper.GetInputDataObject(0, 0)
+    else:
+        logger.debug("This mapper does not have GetInputDataObject method")
+
+    if dataObject:
+        dataObjectId = "%s-dataset" % mapperId
+        dataObjectInstance = serializeInstance(
+            mapper, dataObject, dataObjectId, context, depth + 1
+        )
+
+        if dataObjectInstance:
+            dependencies.append(dataObjectInstance)
+            calls.append(["setInputData", [wrapId(dataObjectId)]])
+
+    if dataObjectInstance:
+        return {
+            "parent": getReferenceId(parent),
+            "id": mapperId,
+            "type": class_name(mapper),
+            "properties": {
+                # VolumeMapper
+                "sampleDistance": mapper.GetSampleDistance(),
+                "imageSampleDistance": mapper.GetImageSampleDistance(),
+                # "maximumSamplesPerRay": mapper.GetMaximumSamplesPerRay(),
+                "autoAdjustSampleDistances": mapper.GetAutoAdjustSampleDistances(),
+                "blendMode": mapper.GetBlendMode(),
+                # "ipScalarRange": mapper.GetIpScalarRange(),
+                # "filterMode": mapper.GetFilterMode(),
+                # "preferSizeOverAccuracy": mapper.Get(),
+            },
+            "calls": calls,
+            "dependencies": dependencies,
+        }
+
+    return None
+
+# -----------------------------------------------------------------------------
+
+
+def lookupTableSerializer(parent, lookupTable, lookupTableId, context, depth):
+    # No children in this case, so no additions to bindings and return empty list
+    # But we do need to add instance
+
+    lookupTableRange = lookupTable.GetRange()
+
+    lookupTableHueRange = [0.5, 0]
+    if hasattr(lookupTable, "GetHueRange"):
+        try:
+            lookupTable.GetHueRange(lookupTableHueRange)
+        except Exception as inst:
+            pass
+
+    lutSatRange = lookupTable.GetSaturationRange()
+    lutAlphaRange = lookupTable.GetAlphaRange()
+
+    return {
+        "parent": getReferenceId(parent),
+        "id": lookupTableId,
+        "type": class_name(lookupTable),
+        "properties": {
+            "numberOfColors": lookupTable.GetNumberOfColors(),
+            "valueRange": lookupTableRange,
+            "hueRange": lookupTableHueRange,
+            # 'alphaRange': lutAlphaRange,  # Causes weird rendering artifacts on client
+            "saturationRange": lutSatRange,
+            "nanColor": lookupTable.GetNanColor(),
+            "belowRangeColor": lookupTable.GetBelowRangeColor(),
+            "aboveRangeColor": lookupTable.GetAboveRangeColor(),
+            "useAboveRangeColor": True
+            if lookupTable.GetUseAboveRangeColor()
+            else False,
+            "useBelowRangeColor": True
+            if lookupTable.GetUseBelowRangeColor()
+            else False,
+            "alpha": lookupTable.GetAlpha(),
+            "vectorSize": lookupTable.GetVectorSize(),
+            "vectorComponent": lookupTable.GetVectorComponent(),
+            "vectorMode": lookupTable.GetVectorMode(),
+            "indexedLookup": lookupTable.GetIndexedLookup(),
+        },
+    }
+
+
+# -----------------------------------------------------------------------------
+
+
+def lookupTableToColorTransferFunction(lookupTable):
+    dataTable = lookupTable.GetTable()
+    table = dataTableToList(dataTable)
+    if table:
+        ctf = vtkColorTransferFunction()
+        tableRange = lookupTable.GetTableRange()
+        points = linspace(*tableRange, num=len(table))
+        for x, rgba in zip(points, table):
+            ctf.AddRGBPoint(x, *[x / 255 for x in rgba[:3]])
+
+        return ctf
+
+    return None
+
+
+def lookupTableSerializer2(parent, lookupTable, lookupTableId, context, depth):
+    ctf = lookupTableToColorTransferFunction(lookupTable)
+    if ctf:
+        return colorTransferFunctionSerializer(
+            parent, ctf, lookupTableId, context, depth
+        )
+
+    return None
+
+
+# -----------------------------------------------------------------------------
+
+
+def propertySerializer(parent, propObj, propObjId, context, depth):
+    representation = (
+        propObj.GetRepresentation() if hasattr(propObj, "GetRepresentation") else 2
+    )
+    colorToUse = (
+        propObj.GetDiffuseColor() if hasattr(propObj, "GetDiffuseColor") else [1, 1, 1]
+    )
+    if representation == 1 and hasattr(propObj, "GetColor"):
+        colorToUse = propObj.GetColor()
+
+    return {
+        "parent": getReferenceId(parent),
+        "id": propObjId,
+        "type": class_name(propObj),
+        "properties": {
+            "representation": representation,
+            "diffuseColor": colorToUse,
+            "color": propObj.GetColor(),
+            "ambientColor": propObj.GetAmbientColor(),
+            "specularColor": propObj.GetSpecularColor(),
+            "edgeColor": propObj.GetEdgeColor(),
+            "ambient": propObj.GetAmbient(),
+            "diffuse": propObj.GetDiffuse(),
+            "specular": propObj.GetSpecular(),
+            "specularPower": propObj.GetSpecularPower(),
+            "opacity": propObj.GetOpacity(),
+            "interpolation": propObj.GetInterpolation(),
+            "edgeVisibility": 1 if propObj.GetEdgeVisibility() else 0,
+            "backfaceCulling": 1 if propObj.GetBackfaceCulling() else 0,
+            "frontfaceCulling": 1 if propObj.GetFrontfaceCulling() else 0,
+            "pointSize": propObj.GetPointSize(),
+            "lineWidth": propObj.GetLineWidth(),
+            "lighting": 1 if propObj.GetLighting() else 0,
+        },
+    }
+
+def volumePropertySerializer(parent, propObj, propObjId, context, depth):
+    calls = []
+    dependencies = []
+
+    # Color handling
+    lut = propObj.GetRGBTransferFunction()
+    if lut:
+        lookupTableId = getReferenceId(lut)
+        lookupTableInstance = serializeInstance(
+            propObj, lut, lookupTableId, context, depth + 1
+        )
+
+        if lookupTableInstance:
+            dependencies.append(lookupTableInstance)
+            calls.append(["setRGBTransferFunction", [0, wrapId(lookupTableId)]])
+
+    # Piecewise handling
+    pwf = propObj.GetScalarOpacity()
+    if pwf:
+        pwfId = getReferenceId(pwf)
+        pwfInstance = serializeInstance(
+            propObj, pwf, pwfId, context, depth + 1
+        )
+
+        if pwfInstance:
+            dependencies.append(pwfInstance)
+            calls.append(["setScalarOpacity", [0, wrapId(pwfId)]])
+
+    return {
+        "parent": getReferenceId(parent),
+        "id": propObjId,
+        "type": class_name(propObj),
+        "properties": {
+            "independentComponents": propObj.GetIndependentComponents(),
+            "interpolationType": propObj.GetInterpolationType(),
+            "shade": propObj.GetShade(),
+            "ambient": propObj.GetAmbient(),
+            "diffuse": propObj.GetDiffuse(),
+            "specular": propObj.GetSpecular(),
+            "specularPower": propObj.GetSpecularPower(),
+            # "useLabelOutline": propObj.GetUseLabelOutline(),
+            # "labelOutlineThickness": propObj.GetLabelOutlineThickness(),
+        },
+        "calls": calls,
+        "dependencies": dependencies,
+    }
+
+# -----------------------------------------------------------------------------
+
+
+def imagedataSerializer(parent, dataset, datasetId, context, depth, requested_fields = ["Normals", "TCoords"]):
+    if hasattr(dataset, "GetDirectionMatrix"):
+        direction = [dataset.GetDirectionMatrix().GetElement(0, i) for i in range(9)]
+    else:
+        direction = [1, 0, 0, 0, 1, 0, 0, 0, 1]
+
+    # Extract dataset fields
+    fields = []
+    extractRequiredFields(fields, parent, dataset, context, "*")
+
+    return {
+        "parent": getReferenceId(parent),
+        "id": datasetId,
+        "type": class_name(dataset),
+        "properties": {
+            "spacing": dataset.GetSpacing(),
+            "origin": dataset.GetOrigin(),
+            "dimensions": dataset.GetDimensions(),
+            "direction": direction,
+            "fields": fields,
+        },
+    }
+
+
+# -----------------------------------------------------------------------------
+
+
+def polydataSerializer(parent, dataset, datasetId, context, depth, requested_fields = ["Normals", "TCoords"]):
+    if dataset and dataset.GetPoints():
+        properties = {}
+
+        # Points
+        points = getArrayDescription(dataset.GetPoints().GetData(), context)
+        points["vtkClass"] = "vtkPoints"
+        properties["points"] = points
+
+        # Verts
+        if dataset.GetVerts() and dataset.GetVerts().GetData().GetNumberOfTuples() > 0:
+            _verts = getArrayDescription(dataset.GetVerts().GetData(), context)
+            properties["verts"] = _verts
+            properties["verts"]["vtkClass"] = "vtkCellArray"
+
+        # Lines
+        if dataset.GetLines() and dataset.GetLines().GetData().GetNumberOfTuples() > 0:
+            _lines = getArrayDescription(dataset.GetLines().GetData(), context)
+            properties["lines"] = _lines
+            properties["lines"]["vtkClass"] = "vtkCellArray"
+
+        # Polys
+        if dataset.GetPolys() and dataset.GetPolys().GetData().GetNumberOfTuples() > 0:
+            _polys = getArrayDescription(dataset.GetPolys().GetData(), context)
+            properties["polys"] = _polys
+            properties["polys"]["vtkClass"] = "vtkCellArray"
+
+        # Strips
+        if (
+            dataset.GetStrips()
+            and dataset.GetStrips().GetData().GetNumberOfTuples() > 0
+        ):
+            _strips = getArrayDescription(dataset.GetStrips().GetData(), context)
+            properties["strips"] = _strips
+            properties["strips"]["vtkClass"] = "vtkCellArray"
+
+        # Fields
+        properties["fields"] = []
+        extractRequiredFields(properties["fields"], parent, dataset, context, requested_fields)
+
+        return {
+            "parent": getReferenceId(parent),
+            "id": datasetId,
+            "type": class_name(dataset),
+            "properties": properties,
+        }
+
+    logger.debug("This dataset has no points!")
+    return None
+
+
+# -----------------------------------------------------------------------------
+
+
+def mergeToPolydataSerializer(parent, dataObject, dataObjectId, context, depth, requested_fields=["Normals", "TCoords"]):
+    dataset = None
+
+    if dataObject.IsA("vtkCompositeDataSet"):
+        gf = vtkCompositeDataGeometryFilter()
+        gf.SetInputData(dataObject)
+        gf.Update()
+        dataset = gf.GetOutput()
+    elif dataObject.IsA("vtkUnstructuredGrid"):
+        gf = vtkDataSetSurfaceFilter()
+        gf.SetInputData(dataObject)
+        gf.Update()
+        dataset = gf.GetOutput()
+    else:
+        dataset = mapper.GetInput()
+
+    return polydataSerializer(parent, dataset, dataObjectId, context, depth, requested_fields)
+
+
+# -----------------------------------------------------------------------------
+
+
+def colorTransferFunctionSerializer(parent, instance, objId, context, depth):
+    nodes = []
+
+    for i in range(instance.GetSize()):
+        # x, r, g, b, midpoint, sharpness
+        node = [0, 0, 0, 0, 0, 0]
+        instance.GetNodeValue(i, node)
+        nodes.append(node)
+
+    return {
+        "parent": getReferenceId(parent),
+        "id": objId,
+        "type": class_name(instance),
+        "properties": {
+            "clamping": 1 if instance.GetClamping() else 0,
+            "colorSpace": instance.GetColorSpace(),
+            "hSVWrap": 1 if instance.GetHSVWrap() else 0,
+            # 'nanColor': instance.GetNanColor(),                  # Breaks client
+            # 'belowRangeColor': instance.GetBelowRangeColor(),    # Breaks client
+            # 'aboveRangeColor': instance.GetAboveRangeColor(),    # Breaks client
+            # 'useAboveRangeColor': 1 if instance.GetUseAboveRangeColor() else 0,
+            # 'useBelowRangeColor': 1 if instance.GetUseBelowRangeColor() else 0,
+            "allowDuplicateScalars": 1 if instance.GetAllowDuplicateScalars() else 0,
+            "alpha": instance.GetAlpha(),
+            "vectorComponent": instance.GetVectorComponent(),
+            "vectorSize": instance.GetVectorSize(),
+            "vectorMode": instance.GetVectorMode(),
+            "indexedLookup": instance.GetIndexedLookup(),
+            "nodes": nodes,
+        },
+    }
+
+def discretizableColorTransferFunctionSerializer(parent, instance, objId, context, depth):
+    ctf = colorTransferFunctionSerializer(parent, instance, objId, context, depth)
+    ctf["properties"]["discretize"] = instance.GetDiscretize()
+    ctf["properties"]["numberOfValues"] = instance.GetNumberOfValues()
+    return ctf
+
+# -----------------------------------------------------------------------------
+
+def pwfSerializer(parent, instance, objId, context, depth):
+    nodes = []
+
+    for i in range(instance.GetSize()):
+        # x, y, midpoint, sharpness
+        node = [0, 0, 0, 0]
+        instance.GetNodeValue(i, node)
+        nodes.append(node)
+
+    return {
+        "parent": getReferenceId(parent),
+        "id": objId,
+        "type": class_name(instance),
+        "properties": {
+            "range": list(instance.GetRange()),
+            "clamping": instance.GetClamping(),
+            "allowDuplicateScalars": instance.GetAllowDuplicateScalars(),
+            "nodes": nodes,
+        },
+    }
+
+# -----------------------------------------------------------------------------
+
+def cubeAxesSerializer(parent, actor, actorId, context, depth):
+    """
+    Possible add-on properties for vtk.js:
+        gridLines: True,
+        axisLabels: None,
+        axisTitlePixelOffset: 35.0,
+        axisTextStyle: {
+            fontColor: 'white',
+            fontStyle: 'normal',
+            fontSize: 18,
+            fontFamily: 'serif',
+        },
+        tickLabelPixelOffset: 12.0,
+        tickTextStyle: {
+            fontColor: 'white',
+            fontStyle: 'normal',
+            fontSize: 14,
+            fontFamily: 'serif',
+        },
+    """
+    axisLabels = ["", "", ""]
+    if actor.GetXAxisLabelVisibility():
+        axisLabels[0] = actor.GetXTitle()
+    if actor.GetYAxisLabelVisibility():
+        axisLabels[1] = actor.GetYTitle()
+    if actor.GetZAxisLabelVisibility():
+        axisLabels[2] = actor.GetZTitle()
+
+    return {
+        "parent": getReferenceId(parent),
+        "id": actorId,
+        "type": "vtkCubeAxesActor",
+        "properties": {
+            # vtkProp
+            "visibility": actor.GetVisibility(),
+            "pickable": actor.GetPickable(),
+            "dragable": actor.GetDragable(),
+            "useBounds": actor.GetUseBounds(),
+            # vtkProp3D
+            "origin": actor.GetOrigin(),
+            "position": actor.GetPosition(),
+            "scale": actor.GetScale(),
+            # vtkActor
+            "forceOpaque": actor.GetForceOpaque(),
+            "forceTranslucent": actor.GetForceTranslucent(),
+            # vtkCubeAxesActor
+            "dataBounds": actor.GetBounds(),
+            "faceVisibilityAngle": 8,
+            "gridLines": True,
+            "axisLabels": axisLabels,
+            "axisTitlePixelOffset": 35.0,
+            "axisTextStyle": {
+                "fontColor": "white",
+                "fontStyle": "normal",
+                "fontSize": 18,
+                "fontFamily": "serif",
+            },
+            "tickLabelPixelOffset": 12.0,
+            "tickTextStyle": {
+                "fontColor": "white",
+                "fontStyle": "normal",
+                "fontSize": 14,
+                "fontFamily": "serif",
+            },
+        },
+        "calls": [["setCamera", [wrapId(getReferenceId(actor.GetCamera()))]]],
+        "dependencies": [],
+    }
+
+# -----------------------------------------------------------------------------
+
+def scalarBarActorSerializer(parent, actor, actorId, context, depth):
+    dependencies = []
+    calls = []
+    lut = actor.GetLookupTable()
+    if not lut:
+        return None
+
+    lutId = getReferenceId(lut)
+    lutInstance = serializeInstance(actor, lut, lutId, context, depth + 1)
+    if not lutInstance:
+        return None
+
+    dependencies.append(lutInstance)
+    calls.append(["setScalarsToColors", [wrapId(lutId)]])
+
+    prop = None
+    if hasattr(actor, "GetProperty"):
+        prop = actor.GetProperty()
+    else:
+        logger.debug("This scalarBarActor does not have a GetProperty method")
+
+        if prop:
+            propId = getReferenceId(prop)
+            propertyInstance = serializeInstance(
+                actor, prop, propId, context, depth + 1
+            )
+            if propertyInstance:
+                dependencies.append(propertyInstance)
+                calls.append(["setProperty", [wrapId(propId)]])
+
+    axisLabel = actor.GetTitle()
+    width = actor.GetWidth()
+    height = actor.GetHeight()
+
+    return {
+        "parent": getReferenceId(parent),
+        "id": actorId,
+        "type": "vtkScalarBarActor",
+        "properties": {
+            # vtkProp
+            "visibility": actor.GetVisibility(),
+            "pickable": actor.GetPickable(),
+            "dragable": actor.GetDragable(),
+            "useBounds": actor.GetUseBounds(),
+            # vtkActor2D
+            # "position": actor.GetPosition(),
+            # "position2": actor.GetPosition2(),
+            # "width": actor.GetWidth(),
+            # "height": actor.GetHeight(),
+            # vtkScalarBarActor
+            "automated": True,
+            "axisLabel": axisLabel,
+            # 'barPosition': [0, 0],
+            # 'barSize': [0, 0],
+            "boxPosition": [0.88, -0.92],
+            "boxSize": [width, height],
+            "axisTitlePixelOffset": 36.0,
+            "axisTextStyle": {
+                "fontColor": actor.GetTitleTextProperty().GetColor(),
+                "fontStyle": "normal",
+                "fontSize": 18,
+                "fontFamily": "serif",
+            },
+            "tickLabelPixelOffset": 14.0,
+            "tickTextStyle": {
+                "fontColor": actor.GetTitleTextProperty().GetColor(),
+                "fontStyle": "normal",
+                "fontSize": 14,
+                "fontFamily": "serif",
+            },
+            "drawNanAnnotation": actor.GetDrawNanAnnotation(),
+            "drawBelowRangeSwatch": actor.GetDrawBelowRangeSwatch(),
+            "drawAboveRangeSwatch": actor.GetDrawAboveRangeSwatch(),
+        },
+        "calls": calls,
+        "dependencies": dependencies,
+    }
+
+# -----------------------------------------------------------------------------
+
+
+def rendererSerializer(parent, instance, objId, context, depth):
+    dependencies = []
+    viewPropIds = []
+    lightsIds = []
+    calls = []
+
+    # Camera
+    camera = instance.GetActiveCamera()
+    cameraId = getReferenceId(camera)
+    cameraInstance = serializeInstance(instance, camera, cameraId, context, depth + 1)
+    if cameraInstance:
+        dependencies.append(cameraInstance)
+        calls.append(["setActiveCamera", [wrapId(cameraId)]])
+
+    # View prop as representation containers
+    viewPropCollection = instance.GetViewProps()
+    for rpIdx in range(viewPropCollection.GetNumberOfItems()):
+        viewProp = viewPropCollection.GetItemAsObject(rpIdx)
+        viewPropId = getReferenceId(viewProp)
+
+        viewPropInstance = serializeInstance(
+            instance, viewProp, viewPropId, context, depth + 1
+        )
+        if viewPropInstance:
+            dependencies.append(viewPropInstance)
+            viewPropIds.append(viewPropId)
+
+    calls += context.buildDependencyCallList(
+        "%s-props" % objId, viewPropIds, "addViewProp", "removeViewProp"
+    )
+
+    # Lights
+    lightCollection = instance.GetLights()
+    for lightIdx in range(lightCollection.GetNumberOfItems()):
+        light = lightCollection.GetItemAsObject(lightIdx)
+        lightId = getReferenceId(light)
+
+        lightInstance = serializeInstance(instance, light, lightId, context, depth + 1)
+        if lightInstance:
+            dependencies.append(lightInstance)
+            lightsIds.append(lightId)
+
+    calls += context.buildDependencyCallList(
+        "%s-lights" % objId, lightsIds, "addLight", "removeLight"
+    )
+
+    if len(dependencies) > 1:
+        return {
+            "parent": getReferenceId(parent),
+            "id": objId,
+            "type": class_name(instance),
+            "properties": {
+                "background": instance.GetBackground(),
+                "background2": instance.GetBackground2(),
+                "viewport": instance.GetViewport(),
+                # These commented properties do not yet have real setters in vtk.js
+                # 'gradientBackground': instance.GetGradientBackground(),
+                # 'aspect': instance.GetAspect(),
+                # 'pixelAspect': instance.GetPixelAspect(),
+                # 'ambient': instance.GetAmbient(),
+                "twoSidedLighting": instance.GetTwoSidedLighting(),
+                "lightFollowCamera": instance.GetLightFollowCamera(),
+                "layer": instance.GetLayer(),
+                "preserveColorBuffer": instance.GetPreserveColorBuffer(),
+                "preserveDepthBuffer": instance.GetPreserveDepthBuffer(),
+                "nearClippingPlaneTolerance": instance.GetNearClippingPlaneTolerance(),
+                "clippingRangeExpansion": instance.GetClippingRangeExpansion(),
+                "useShadows": instance.GetUseShadows(),
+                "useDepthPeeling": instance.GetUseDepthPeeling(),
+                "occlusionRatio": instance.GetOcclusionRatio(),
+                "maximumNumberOfPeels": instance.GetMaximumNumberOfPeels(),
+                "interactive": instance.GetInteractive(),
+            },
+            "dependencies": dependencies,
+            "calls": calls,
+        }
+
+    return None
+
+
+# -----------------------------------------------------------------------------
+
+
+def cameraSerializer(parent, instance, objId, context, depth):
+    return {
+        "parent": getReferenceId(parent),
+        "id": objId,
+        "type": class_name(instance),
+        "properties": {
+            "focalPoint": instance.GetFocalPoint(),
+            "position": instance.GetPosition(),
+            "viewUp": instance.GetViewUp(),
+            "clippingRange": instance.GetClippingRange(),
+        },
+    }
+
+
+# -----------------------------------------------------------------------------
+
+
+def lightTypeToString(value):
+    """
+    #define VTK_LIGHT_TYPE_HEADLIGHT    1
+    #define VTK_LIGHT_TYPE_CAMERA_LIGHT 2
+    #define VTK_LIGHT_TYPE_SCENE_LIGHT  3
+
+    'HeadLight';
+    'SceneLight';
+    'CameraLight'
+    """
+    if value == 1:
+        return "HeadLight"
+    elif value == 2:
+        return "CameraLight"
+
+    return "SceneLight"
+
+
+def lightSerializer(parent, instance, objId, context, depth):
+    return {
+        "parent": getReferenceId(parent),
+        "id": objId,
+        "type": class_name(instance),
+        "properties": {
+            # 'specularColor': instance.GetSpecularColor(),
+            # 'ambientColor': instance.GetAmbientColor(),
+            "switch": instance.GetSwitch(),
+            "intensity": instance.GetIntensity(),
+            "color": instance.GetDiffuseColor(),
+            "position": instance.GetPosition(),
+            "focalPoint": instance.GetFocalPoint(),
+            "positional": instance.GetPositional(),
+            "exponent": instance.GetExponent(),
+            "coneAngle": instance.GetConeAngle(),
+            "attenuationValues": instance.GetAttenuationValues(),
+            "lightType": lightTypeToString(instance.GetLightType()),
+            "shadowAttenuation": instance.GetShadowAttenuation(),
+        },
+    }
+
+
+# -----------------------------------------------------------------------------
+
+
+def renderWindowSerializer(parent, instance, objId, context, depth):
+    dependencies = []
+    rendererIds = []
+
+    rendererCollection = instance.GetRenderers()
+    for rIdx in range(rendererCollection.GetNumberOfItems()):
+        # Grab the next vtkRenderer
+        renderer = rendererCollection.GetItemAsObject(rIdx)
+        rendererId = getReferenceId(renderer)
+        rendererInstance = serializeInstance(
+            instance, renderer, rendererId, context, depth + 1
+        )
+        if rendererInstance:
+            dependencies.append(rendererInstance)
+            rendererIds.append(rendererId)
+
+    calls = context.buildDependencyCallList(
+        objId, rendererIds, "addRenderer", "removeRenderer"
+    )
+
+    return {
+        "parent": getReferenceId(parent),
+        "id": objId,
+        "type": class_name(instance),
+        "properties": {"numberOfLayers": instance.GetNumberOfLayers()},
+        "dependencies": dependencies,
+        "calls": calls,
+        "mtime": instance.GetMTime(),
+    }
diff --git a/Web/Python/vtkmodules/web/testing.py b/Web/Python/vtkmodules/web/testing.py
new file mode 100644 (file)
index 0000000..8b74de5
--- /dev/null
@@ -0,0 +1,788 @@
+r"""
+    This module provides some testing functionality for paraview and
+    vtk web applications.  It provides the ability to run an arbitrary
+    test script in a separate thread and communicate the results back
+    to the service so that the CTest framework can be notified of the
+    success or failure of the test.
+
+    This test harness will notice when the test script has finished
+    running and will notify the service to stop.  At this point, the
+    test results will be checked in the main thread which ran the
+    service, and in the case of failure an exception will be raised
+    to notify CTest of the failure.
+
+    Test scripts need to follow some simple rules in order to work
+    within the test harness framework:
+
+    1) implement a function called "runTest(args)", where the args
+    parameter contains all the arguments given to the web application
+    upon starting.  Among other important items, args will contain the
+    port number where the web application is listening.
+
+    2) import the testing module so that the script has access to
+    the functions which indicate success and failure.  Also the
+    testing module contains convenience functions that might be of
+    use to the test scripts.
+
+       from vtk.web import testing
+
+    3) Call the "testPass(testName)" or "testFail(testName)" functions
+    from within the runTest(args) function to indicate to the framework
+    whether the test passed or failed.
+
+"""
+
+import_warning_info = ""
+test_module_comm_queue = None
+
+from vtkmodules.vtkTestingRendering import vtkTesting
+
+# Try standard Python imports
+try:
+    import os, re, time, datetime, threading, imp, inspect, Queue, types, io
+except:
+    import_warning_info += "\nUnable to load at least one basic Python module"
+
+# Image comparison imports
+try:
+    try:
+        from PIL import Image
+    except ImportError:
+        import Image
+    except:
+        raise
+    import base64
+    import itertools
+except:
+    import_warning_info += (
+        "\nUnable to load at least one modules necessary for image comparison"
+    )
+
+# Browser testing imports
+try:
+    import selenium
+    from selenium import webdriver
+except:
+    import_warning_info += (
+        "\nUnable to load at least one module necessary for browser tests"
+    )
+
+# HTTP imports
+try:
+    import requests
+except:
+    import_warning_info += (
+        "\nUnable to load at least one module necessary for HTTP tests"
+    )
+
+
+# Define some infrastructure to support different (or no) browsers
+test_module_browsers = ["firefox", "chrome", "internet_explorer", "safari", "nobrowser"]
+
+
+class TestModuleBrowsers:
+    firefox, chrome, internet_explorer, safari, nobrowser = range(5)
+
+
+# =============================================================================
+# We can use this exception type to indicate that the test shouldn't actually
+# "fail", rather that it was unable to run because some dependencies were not
+# met.
+# =============================================================================
+class DependencyError(Exception):
+    def __init__(self, value):
+        self.value = value
+
+    def __str__(self):
+        return repr(self.value)
+
+
+# =============================================================================
+# This class allows usage as a dictionary and an object with named property
+# access.
+# =============================================================================
+class Dictionary(dict):
+    def __getattribute__(self, attrName):
+        return self[attrName]
+
+    def __setattr__(self, attrName, attrValue):
+        self[attrName] = attrValue
+
+
+# =============================================================================
+# Checks whether test script supplied, if so, safely imports needed modules
+# =============================================================================
+def initialize(opts, reactor=None, cleanupMethod=None):
+    """
+    This function should be called to initialize the testing module.  The first
+    important thing it does is to store the options for later, since the
+    startTestThread function will need them.  Then it checks the arguments that
+    were passed into the server to see if a test was actually requested, making
+    a note of this fact.  Then, if a test was required, this function then
+    checks if all the necessary testing modules were safely imported, printing
+    a warning if not.  If tests were requested and all modules were present,
+    then this function sets "test_module_do_testing" to True and sets up the
+    startTestThread function to be called after the reactor is running.
+
+        opts: Parsed arguments from the server
+
+        reactor: This argument is optional, but is used by server.py to
+        cause the test thread to be started only after the server itself
+        has started.  If it is not provided, the test thread is launched
+        immediately.
+
+        cleanupMethod: A callback method you would like the test thread
+        to execute when the test has finished.  This is used by server.py
+        as a way to have the server terminated after the test has finished,
+        but could be used for other cleanup purposes.  This argument is
+        also optional.
+    """
+
+    global import_warning_info
+
+    global testModuleOptions
+    testModuleOptions = Dictionary()
+
+    # Copy the testing options into something we can easily extend
+    for arg in vars(opts):
+        optValue = getattr(opts, arg)
+        testModuleOptions[arg] = optValue
+
+    # If we got one, add the cleanup method to the testing options
+    if cleanupMethod:
+        testModuleOptions["cleanupMethod"] = cleanupMethod
+
+    # Check if a test was actually requested
+    if (
+        testModuleOptions.testScriptPath != ""
+        and testModuleOptions.testScriptPath is not None
+    ):
+        # Check if we ran into trouble with any of the testing imports
+        if import_warning_info != "":
+            print("WARNING: Some tests may have unmet dependencies")
+            print(import_warning_info)
+
+        if reactor is not None:
+            # Add startTest callback to the reactor callback queue, so that
+            # the test thread gets started after the reactor is running.  Of
+            # course this should only happen if everything is good for tests.
+            reactor.callWhenRunning(_start_test_thread)
+        else:
+            # Otherwise, our aim is to start the thread from another process
+            # so just call the start method.
+            _start_test_thread()
+
+
+# =============================================================================
+# Grab out the command-line arguments needed for by the testing module.
+# =============================================================================
+def add_arguments(parser):
+    """
+    This function retrieves any command-line arguments that the client-side
+    tester needs.  In order to run a test, you will typically just need the
+    following:
+
+      --run-test-script => This should be the full path to the test script to
+      be run.
+
+      --baseline-img-dir => This should be the 'Baseline' directory where the
+      baseline images for this test are located.
+
+      --test-use-browser => This should be one of the supported browser types,
+      or else 'nobrowser'.  The choices are 'chrome', 'firefox', 'internet_explorer',
+      'safari', or 'nobrowser'.
+    """
+
+    parser.add_argument(
+        "--run-test-script",
+        default="",
+        help="The path to a test script to run",
+        dest="testScriptPath",
+    )
+
+    parser.add_argument(
+        "--baseline-img-dir",
+        default="",
+        help="The path to the directory containing the web test baseline images",
+        dest="baselineImgDir",
+    )
+
+    parser.add_argument(
+        "--test-use-browser",
+        default="nobrowser",
+        help="One of 'chrome', 'firefox', 'internet_explorer', 'safari', or 'nobrowser'.",
+        dest="useBrowser",
+    )
+
+    parser.add_argument(
+        "--temporary-directory",
+        default=".",
+        help="A temporary directory for storing test images and diffs",
+        dest="tmpDirectory",
+    )
+
+    parser.add_argument(
+        "--test-image-file-name",
+        default="",
+        help="Name of file in which to store generated test image",
+        dest="testImgFile",
+    )
+
+
+# =============================================================================
+# Initialize the test client
+# =============================================================================
+def _start_test_thread():
+    """
+    This function checks whether testing is required and if so, sets up a Queue
+    for the purpose of communicating with the thread.  then it starts the
+    after waiting 5 seconds for the server to have a chance to start up.
+    """
+
+    global test_module_comm_queue
+    test_module_comm_queue = Queue.Queue()
+
+    t = threading.Thread(
+        target=launch_web_test,
+        args=[],
+        kwargs={
+            "serverOpts": testModuleOptions,
+            "commQueue": test_module_comm_queue,
+            "testScript": testModuleOptions.testScriptPath,
+        },
+    )
+
+    t.start()
+
+
+# =============================================================================
+# Test scripts call this function to indicate passage of their test
+# =============================================================================
+def test_pass(testName):
+    """
+    Test scripts should call this function to indicate that the test passed.  A
+    note is recorded that the test succeeded, and is checked later on from the
+    main thread so that CTest can be notified of this result.
+    """
+
+    global test_module_comm_queue
+    resultObj = {testName: "pass"}
+    test_module_comm_queue.put(resultObj)
+
+
+# =============================================================================
+# Test scripts call this function to indicate failure of their test
+# =============================================================================
+def test_fail(testName):
+    """
+    Test scripts should call this function to indicate that the test failed.  A
+    note is recorded that the test did not succeed, and this note is checked
+    later from the main thread so that CTest can be notified of the result.
+
+    The main thread is the only one that can signal test failure in
+    CTest framework, and the main thread won't have a chance to check for
+    passage or failure of the test until the main loop has terminated.  So
+    here we just record the failure result, then we check this result in the
+    processTestResults() function, throwing an exception at that point to
+    indicate to CTest that the test failed.
+    """
+
+    global test_module_comm_queue
+    resultObj = {testName: "fail"}
+    test_module_comm_queue.put(resultObj)
+
+
+# =============================================================================
+# Concatenate any number of strings into a single path string.
+# =============================================================================
+def concat_paths(*pathElts):
+    """
+    A very simple convenience function so that test scripts can build platform
+    independent paths out of a list of elements, without having to import the
+    os module.
+
+        pathElts: Any number of strings which should be concatenated together
+        in a platform independent manner.
+    """
+
+    return os.path.join(*pathElts)
+
+
+# =============================================================================
+# So we can change our time format in a single place, this function is
+# provided.
+# =============================================================================
+def get_current_time_string():
+    """
+    This function returns the current time as a string, using ISO 8601 format.
+    """
+
+    return datetime.datetime.now().isoformat(" ")
+
+
+# =============================================================================
+# Uses vtkTesting to compare images.  According to comments in the vtkTesting
+# C++ code (and this seems to work), if there are multiple baseline images in
+# the same directory as the baseline_img, and they follow the naming pattern:
+# 'img.png', 'img_1.png', ... , 'img_N.png', then all of these images will be
+# tried for a match.
+# =============================================================================
+def compare_images(test_img, baseline_img, tmp_dir="."):
+    """
+    This function creates a vtkTesting object, and specifies the name of the
+    baseline image file, using a fully qualified path (baseline_img must be
+    fully qualified).  Then it calls the vtkTesting method which compares the
+    image (test_img, specified only with a relative path) against the baseline
+    image as well as any other images in the same directory as the baseline
+    image which follow the naming pattern: 'img.png', 'img_1.png', ... , 'img_N.png'
+
+        test_img: File name of output image to be compared against baseline.
+
+        baseline_img: Fully qualified path to first of the baseline images.
+
+        tmp_dir: Fully qualified path to a temporary directory for storing images.
+    """
+
+    # Create a vtkTesting object and specify a baseline image
+    t = vtkTesting()
+    t.AddArgument("-T")
+    t.AddArgument(tmp_dir)
+    t.AddArgument("-V")
+    t.AddArgument(baseline_img)
+
+    # Perform the image comparison test and print out the result.
+    return t.RegressionTest(test_img, 0.05)
+
+
+# =============================================================================
+# Provide a wait function
+# =============================================================================
+def wait_with_timeout(delay=None, limit=0, criterion=None):
+    """
+    This function provides the ability to wait for a certain number of seconds,
+    or else to wait for a specific criterion to be met.
+    """
+    for i in itertools.count():
+        if criterion is not None and criterion():
+            return True
+        elif delay * i > limit:
+            return False
+        else:
+            time.sleep(delay)
+
+
+# =============================================================================
+# Define a WebTest class with five stages of testing: initialization, setup,
+# capture, postprocess, and cleanup.
+# =============================================================================
+class WebTest(object):
+    """
+    This is the base class for all automated web-based tests.  It defines five
+    stages that any test must run through, and allows any or all of these
+    stages to be overridden by subclasses.  This class defines the run_test
+    method to invoke the five stages overridden by subclasses, one at a time:
+    1) initialize, 2) setup, 3) capture, 4) postprocess, and 5) cleanup.
+    """
+
+    class Abort:
+        pass
+
+    def __init__(self, url=None, testname=None, **kwargs):
+        self.url = url
+        self.testname = testname
+
+    def run_test(self):
+        try:
+            self.checkdependencies()
+            self.initialize()
+            self.setup()
+            self.capture()
+            self.postprocess()
+        except WebTest.Abort:
+            # Placeholder for future option to return failure result
+            pass
+        except:
+            self.cleanup()
+            raise
+
+        self.cleanup()
+
+    def checkdependencies(self):
+        pass
+
+    def initialize(self):
+        pass
+
+    def setup(self):
+        pass
+
+    def capture(self):
+        pass
+
+    def postprocess(self):
+        pass
+
+    def cleanup(self):
+        pass
+
+
+# =============================================================================
+# Define a WebTest subclass designed specifically for browser-based tests.
+# =============================================================================
+class BrowserBasedWebTest(WebTest):
+    """
+    This class can be used as a base for any browser-based web tests.  It
+    introduces the notion of a selenium browser and overrides phases (1) and
+    (3), initialization and cleanup, of the test phases introduced in the base
+    class.  Initialization involves selecting the browser type, setting the
+    browser window size, and asking the browser to load the url.  Cleanup
+    involves closing the browser window.
+    """
+
+    def __init__(self, size=None, browser=None, **kwargs):
+        self.size = size
+        self.browser = browser
+        self.window = None
+
+        WebTest.__init__(self, **kwargs)
+
+    def initialize(self):
+        try:
+            if self.browser is None or self.browser == TestModuleBrowsers.chrome:
+                self.window = webdriver.Chrome()
+            elif self.browser == TestModuleBrowsers.firefox:
+                self.window = webdriver.Firefox()
+            elif self.browser == TestModuleBrowsers.internet_explorer:
+                self.window = webdriver.Ie()
+            else:
+                raise DependencyError(
+                    "self.browser argument has illegal value %r" % (self.browser)
+                )
+        except DependencyError as dErr:
+            raise
+        except Exception as inst:
+            raise DependencyError(inst)
+
+        if self.size is not None:
+            self.window.set_window_size(self.size[0], self.size[1])
+
+        if self.url is not None:
+            self.window.get(self.url)
+
+    def cleanup(self):
+        try:
+            self.window.quit()
+        except:
+            print(
+                "Unable to call window.quit, perhaps this is expected because of unmet browser dependency."
+            )
+
+
+# =============================================================================
+# Extend BrowserBasedWebTest to handle vtk-style image comparison
+# =============================================================================
+class ImageComparatorWebTest(BrowserBasedWebTest):
+    """
+    This class extends browser based web tests to include image comparison.  It
+    overrides the capture phase of testing with some functionality to simply
+    grab a screenshot of the entire browser window.  It overrides the
+    postprocess phase with a call to vtk image comparison functionality.
+    Derived classes can then simply override the setup function with a series
+    of selenium-based browser interactions to create a complete test.  Derived
+    classes may also prefer to override the capture phase to capture only
+    certain portions of the browser window for image comparison.
+    """
+
+    def __init__(self, filename=None, baseline=None, temporaryDir=None, **kwargs):
+        if filename is None:
+            raise TypeError("missing argument 'filename'")
+        if baseline is None:
+            raise TypeError("missing argument 'baseline'")
+
+        BrowserBasedWebTest.__init__(self, **kwargs)
+        self.filename = filename
+        self.baseline = baseline
+        self.tmpDir = temporaryDir
+
+    def capture(self):
+        self.window.save_screenshot(self.filename)
+
+    def postprocess(self):
+        result = compare_images(self.filename, self.baseline, self.tmpDir)
+
+        if result == 1:
+            test_pass(self.testname)
+        else:
+            test_fail(self.testname)
+
+
+# =============================================================================
+# Given a css selector to use in finding the image element, get the element,
+# then base64 decode the "src" attribute and return it.
+# =============================================================================
+def get_image_data(browser, cssSelector):
+    """
+    This function takes a selenium browser and a css selector string and uses
+    them to find the target HTML image element.  The desired image element
+    should contain it's image data as a Base64 encoded JPEG image string.
+    The 'src' attribute of the image is read, Base64-decoded, and then
+    returned.
+
+        browser: A selenium browser instance, as created by webdriver.Chrome(),
+        for example.
+
+        cssSelector: A string containing a CSS selector which will be used to
+        find the HTML image element of interest.
+    """
+
+    # Here's maybe a better way to get at that image element
+    imageElt = browser.find_element_by_css_selector(cssSelector)
+
+    # Now get the Base64 image string and decode it into image data
+    base64String = imageElt.get_attribute("src")
+    b64RegEx = re.compile(r"data:image/jpeg;base64,(.+)")
+    b64Matcher = b64RegEx.match(base64String)
+    imgdata = base64.b64decode(b64Matcher.group(1))
+
+    return imgdata
+
+
+# =============================================================================
+# Combines a variation on above function with the write_image_to_disk function.
+# converting jpg to png in the process, if necessary.
+# =============================================================================
+def save_image_data_as_png(browser, cssSelector, imgfilename):
+    """
+    This function takes a selenium browser instance, a css selector string,
+    and a file name.  It uses the css selector string to finds the target HTML
+    Image element, which should contain a Base64 encoded JPEG image string,
+    it decodes the string to image data, and then saves the data to the file.
+    The image type of the written file is determined from the extension of the
+    provided filename.
+
+        browser: A selenium browser instance as created by webdriver.Chrome(),
+        for example.
+
+        cssSelector: A string containing a CSS selector which will be used to
+        find the HTML image element of interest.
+
+        imgFilename: The filename to which to save the image. The extension is
+        used to determine the type of image which should be saved.
+    """
+    imageElt = browser.find_element_by_css_selector(cssSelector)
+    base64String = imageElt.get_attribute("src")
+    b64RegEx = re.compile(r"data:image/jpeg;base64,(.+)")
+    b64Matcher = b64RegEx.match(base64String)
+    img = Image.open(io.BytesIO(base64.b64decode(b64Matcher.group(1))))
+    img.save(imgfilename)
+
+
+# =============================================================================
+# Given a decoded image and the full path to a file, write the image to the
+# file.
+# =============================================================================
+def write_image_to_disk(imgData, filePath):
+    """
+    This function takes an image data, as returned by this module's
+    get_image_data() function for example, and writes it out to the file given by
+    the filePath parameter.
+
+        imgData: An image data object
+        filePath: The full path, including the file name and extension, where
+        the image should be written.
+    """
+
+    with open(filePath, "wb") as f:
+        f.write(imgData)
+
+
+# =============================================================================
+# There could be problems if the script file has more than one class defn which
+# is a subclass of vtk.web.testing.WebTest, so we should write some
+# documentation to help people avoid that.
+# =============================================================================
+def instantiate_test_subclass(pathToScript, **kwargs):
+    """
+    This function takes the fully qualified path to a test file, along with
+    any needed keyword arguments, then dynamically loads the file as a module
+    and finds the test class defined inside of it via inspection.  It then
+    uses the keyword arguments to instantiate the test class and return the
+    instance.
+
+        pathToScript: Fully qualified path to python file containing defined
+        subclass of one of the test base classes.
+        kwargs: Keyword arguments to be passed to the constructor of the
+        testing subclass.
+    """
+
+    # Load the file as a module
+    moduleName = imp.load_source("dynamicTestModule", pathToScript)
+    instance = None
+
+    # Inspect dynamically loaded module members
+    for name, obj in inspect.getmembers(moduleName):
+        # Looking for classes only
+        if inspect.isclass(obj):
+            instance = obj.__new__(obj)
+            # And only classes defined in the dynamically loaded module
+            if instance.__module__ == "dynamicTestModule":
+                try:
+                    instance.__init__(**kwargs)
+                    break
+                except Exception as inst:
+                    print("Caught exception: " + str(type(inst)))
+                    print(inst)
+                    raise
+
+    return instance
+
+
+# =============================================================================
+# For testing purposes, define a function which can interact with a running
+# paraview or vtk web application service.
+# =============================================================================
+def launch_web_test(*args, **kwargs):
+    """
+    This function loads a python file as a module (with no package), and then
+    instantiates the class it must contain, and finally executes the run_test()
+    method of the class (which the class may override, but which is defined in
+    both of the testing base classes, WebTest and ImageComparatorBaseClass).
+    After the run_test() method finishes, this function will stop the web
+    server if required.  This function expects some keyword arguments will be
+    present in order for it to complete it's task:
+
+        kwargs['serverOpts']: An object containing all the parameters used
+        to start the web service.  Some of them will be used in the test script
+        in order perform the test.  For example, the port on which the server
+        was started will be required in order to connect to the server.
+
+        kwargs['testScript']: The full path to the python file containing the
+        testing subclass.
+    """
+
+    serverOpts = None
+    testScriptFile = None
+
+    # This is really the thing all test scripts will need: access to all
+    # the options used to start the server process.
+    if "serverOpts" in kwargs:
+        serverOpts = kwargs["serverOpts"]
+        # print 'These are the serverOpts we got: '
+        # print serverOpts
+
+    # Get the full path to the test script
+    if "testScript" in kwargs:
+        testScriptFile = kwargs["testScript"]
+
+    testName = "unknown"
+
+    # Check for a test file (python file)
+    if testScriptFile is None:
+        print("No test script file found, no test script will be run.")
+        test_fail(testName)
+
+    # The test name will be generated from the python script name, so
+    # match and capture a bunch of contiguous characters which are
+    # not '.', '\', or '/', followed immediately by the string '.py'.
+    fnamePattern = re.compile("([^\.\/\\\]+)\.py")
+    fmatch = re.search(fnamePattern, testScriptFile)
+    if fmatch:
+        testName = fmatch.group(1)
+    else:
+        print(
+            "Unable to parse testScriptFile ("
+            + str(testScriptfile)
+            + "), no test will be run"
+        )
+        test_fail(testName)
+
+    # If we successfully got a test name, we are ready to try and run the test
+    if testName != "unknown":
+
+        # Output file and baseline file names are generated from the test name
+        imgFileName = testName + ".png"
+        knownGoodFileName = concat_paths(serverOpts.baselineImgDir, imgFileName)
+        tempDir = serverOpts.tmpDirectory
+        testImgFileName = serverOpts.testImgFile
+
+        testBrowser = test_module_browsers.index(serverOpts.useBrowser)
+
+        # Now try to instantiate and run the test
+        try:
+            testInstance = instantiate_test_subclass(
+                testScriptFile,
+                testname=testName,
+                host=serverOpts.host,
+                port=serverOpts.port,
+                browser=testBrowser,
+                filename=testImgFileName,
+                baseline=knownGoodFileName,
+                temporaryDir=tempDir,
+            )
+
+            # If we were able to instantiate the test, run it, otherwise we
+            # consider it a failure.
+            if testInstance is not None:
+                try:
+                    testInstance.run_test()
+                except DependencyError as derr:
+                    # TODO: trigger return SKIP_RETURN_CODE when CMake 3 is required
+                    print(
+                        "Some dependency of this test was not met, allowing it to pass"
+                    )
+                    test_pass(testName)
+            else:
+                print("Unable to instantiate test instance, failing test")
+                test_fail(testName)
+                return
+
+        except Exception as inst:
+            import sys, traceback
+
+            tb = sys.exc_info()[2]
+            print("Caught an exception while running test script:")
+            print("  " + str(type(inst)))
+            print("  " + str(inst))
+            print("  " + "".join(traceback.format_tb(tb)))
+            test_fail(testName)
+
+    # If we were passed a cleanup method to run after testing, invoke it now
+    if "cleanupMethod" in serverOpts:
+        serverOpts["cleanupMethod"]()
+
+
+# =============================================================================
+# To keep the service module clean, we'll process the test results here, given
+# the test result object we generated in "launch_web_test".  It is
+# passed back to this function after the service has completed.  Failure of
+# of the test is indicated by raising an exception in here.
+# =============================================================================
+def finalize():
+    """
+    This function checks the module's global test_module_comm_queue variable for a
+    test result.  If one is found and the result is 'fail', then this function
+    raises an exception to communicate the failure to the CTest framework.
+
+    In order for a test result to be found in the test_module_comm_queue variable,
+    the test script must have called either the testPass or testFail functions
+    provided by this test module before returning.
+    """
+
+    global test_module_comm_queue
+
+    if test_module_comm_queue is not None:
+        resultObject = test_module_comm_queue.get()
+
+        failedATest = False
+
+        for testName in resultObject:
+            testResult = resultObject[testName]
+            if testResult == "fail":
+                print("  Test -> " + testName + ": " + testResult)
+                failedATest = True
+
+        if failedATest is True:
+            raise Exception(
+                "At least one of the requested tests failed.  "
+                + "See detailed output, above, for more information"
+            )
diff --git a/Web/Python/vtkmodules/web/utils.py b/Web/Python/vtkmodules/web/utils.py
new file mode 100644 (file)
index 0000000..1a49112
--- /dev/null
@@ -0,0 +1,211 @@
+try:
+    import numpy as np
+except ImportError:
+    raise ImportError(
+        "This module depends on the numpy module. Please make\
+sure that it is installed properly."
+    )
+
+import base64
+
+from vtkmodules.util.numpy_support import vtk_to_numpy
+from vtkmodules.vtkFiltersGeometry import vtkDataSetSurfaceFilter
+
+# Numpy to JS TypedArray
+to_js_type = {
+    "int8": "Int8Array",
+    "uint8": "Uint8Array",
+    "int16": "Int16Array",
+    "uint16": "Uint16Array",
+    "int32": "Int32Array",
+    "uint32": "Uint32Array",
+    "int64": "Int32Array",
+    "uint64": "Uint32Array",
+    "float32": "Float32Array",
+    "float64": "Float64Array",
+}
+
+
+def b64_encode_numpy(obj):
+    # Convert 1D numpy arrays with numeric types to memoryviews with
+    # datatype and shape metadata.
+    if len(obj) == 0:
+        return obj.tolist()
+
+    dtype = obj.dtype
+    if dtype.kind == "f":
+        return np_encode(obj)
+    elif dtype.kind == "b":
+        return np_encode(obj, np.uint8)
+    elif dtype.kind in ["u", "i"]:
+        # Try to see if we can downsize the array
+        max_value = np.amax(obj)
+        min_value = np.amin(obj)
+        signed = min_value < 0
+        test_value = max(max_value, -min_value)
+        if signed:
+            if test_value < np.iinfo(np.int8):
+                return np_encode(obj, np.int8)
+            if test_value < np.iinfo(np.int16).max:
+                return np_encode(obj, np.int16)
+            if test_value < np.iinfo(np.int32).max:
+                return np_encode(obj, np.int32)
+        else:
+            if test_value < np.iinfo(np.uint8).max:
+                return np_encode(obj, np.uint8)
+            if test_value < np.iinfo(np.uint16).max:
+                return np_encode(obj, np.uint16)
+            if test_value < np.iinfo(np.uint32).max:
+                return np_encode(obj, np.uint32)
+
+    # Convert all other numpy arrays to lists
+    return obj.tolist()
+
+
+def np_encode(array, np_type=None):
+    if np_type:
+        n_array = array.astype(np_type).ravel(order="C")
+        return {
+            "bvals": base64.b64encode(memoryview(n_array)).decode("utf-8"),
+            "dtype": str(n_array.dtype),
+            "shape": list(array.shape),
+        }
+    return {
+        "bvals": base64.b64encode(memoryview(array.ravel(order="C"))).decode("utf-8"),
+        "dtype": str(array.dtype),
+        "shape": list(array.shape),
+    }
+
+
+def mesh_array(array):
+    if array:
+        return b64_encode_numpy(vtk_to_numpy(array.GetData()))
+
+
+def data_array(data_array, location="PointData", name=None):
+    if data_array:
+        dataRange = data_array.GetRange(-1)
+        nb_comp = data_array.GetNumberOfComponents()
+        values = vtk_to_numpy(data_array)
+        js_types = to_js_type[str(values.dtype)]
+        return {
+            "name": name if name else data_array.GetName(),
+            "values": b64_encode_numpy(values),
+            "numberOfComponents": nb_comp,
+            "type": js_types,
+            "location": location,
+            "dataRange": dataRange,
+        }
+
+
+def field_data(field_data, names, location="PointData"):
+    fields = []
+    for name in names:
+        array = field_data.GetArray(name)
+        js_array = data_array(array, location, name)
+        if js_array:
+            fields.append(js_array)
+
+    return fields
+
+
+def mesh(dataset, field_to_keep=None, point_arrays=None, cell_arrays=None):
+    """Expect any dataset and extract its surface into a dash_vtk.Mesh state property"""
+    if dataset is None:
+        return None
+
+    # Make sure we have a polydata to export
+    polydata = None
+    if dataset.IsA("vtkPolyData"):
+        polydata = dataset
+    else:
+        extractSkinFilter = vtkDataSetSurfaceFilter()
+        extractSkinFilter.SetInputData(dataset)
+        extractSkinFilter.Update()
+        polydata = extractSkinFilter.GetOutput()
+
+    if polydata.GetPoints() is None:
+        return None
+
+    # Extract mesh
+    state = {"mesh": {}}
+
+    points = mesh_array(polydata.GetPoints())
+    if points:
+        state["mesh"]["points"] = points
+
+    verts = mesh_array(polydata.GetVerts())
+    if verts:
+        state["mesh"]["verts"] = verts
+
+    lines = mesh_array(polydata.GetLines())
+    if lines:
+        state["mesh"]["lines"] = lines
+
+    polys = mesh_array(polydata.GetPolys())
+    if polys:
+        state["mesh"]["polys"] = polys
+
+    strips = mesh_array(polydata.GetStrips())
+    if strips:
+        state["mesh"]["strips"] = strips
+
+    # Scalars
+    if field_to_keep is not None:
+        field = None
+        p_array = polydata.GetPointData().GetArray(field_to_keep)
+        c_array = polydata.GetCellData().GetArray(field_to_keep)
+
+        if c_array:
+            field = data_array(c_array, location="CellData", name=field_to_keep)
+
+        if p_array:
+            field = data_array(p_array, location="PointData", name=field_to_keep)
+
+        if field:
+            state.update({"field": field})
+
+    # PointData Fields
+    if point_arrays:
+        point_data = field_data(polydata.GetPointData(), point_arrays, "PointData")
+        if len(point_data):
+            state.update({"pointArrays": point_data})
+
+    # CellData Fields
+    if cell_arrays:
+        cell_data = field_data(polydata.GetCellData(), cell_arrays, "CellData")
+        if len(cell_data):
+            state.update({"cellArrays": cell_data})
+
+    return state
+
+
+def volume(dataset):
+    """Expect a vtkImageData and extract its setting for the dash_vtk.Volume state"""
+    if dataset is None or not dataset.IsA("vtkImageData"):
+        return None
+
+    state = {
+        "image": {
+            "dimensions": dataset.GetDimensions(),
+            "spacing": dataset.GetSpacing(),
+            "origin": dataset.GetOrigin(),
+        },
+    }
+
+    # Capture image orientation if any
+    if hasattr(dataset, "GetDirectionMatrix"):
+        matrix = dataset.GetDirectionMatrix()
+        js_mat = []
+        for j in range(3):
+            for i in range(3):
+                js_mat.append(matrix.GetElement(i, j))
+
+        state["image"]["direction"] = js_mat
+
+    scalars = dataset.GetPointData().GetScalars()
+    field = data_array(scalars, location="PointData")
+    if field:
+        state["field"] = field
+
+    return state
diff --git a/Web/Python/vtkmodules/web/venv.py b/Web/Python/vtkmodules/web/venv.py
new file mode 100644 (file)
index 0000000..3bef608
--- /dev/null
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+"""Activate venv for current interpreter:
+
+Use `from vtk.web import venv` along one of the following
+ - `--venv /path/to/venv/base` argument
+ - environment variable `VTK_VENV=/path/to/venv/base`
+
+This can be used when you must use an existing Python interpreter, not the venv bin/python.
+"""
+import os
+import site
+import sys
+
+VENV_BASE = None
+VENV_LOADED = False
+
+if "--venv" in sys.argv:
+    VENV_BASE = os.path.abspath(sys.argv[sys.argv.index("--venv") + 1])
+
+if os.environ.get("VTK_VENV"):
+    VENV_BASE = os.path.abspath(os.environ.get("VTK_VENV"))
+
+if not VENV_LOADED and VENV_BASE and os.path.exists(VENV_BASE):
+    VENV_LOADED = True
+    # Code inspired by virutal-env::bin/active_this.py
+    bin_dir = os.path.join(VENV_BASE, "bin")
+    os.environ["PATH"] = os.pathsep.join([bin_dir] + os.environ.get("PATH", "").split(os.pathsep))
+    os.environ["VIRTUAL_ENV"] = VENV_BASE
+    prev_length = len(sys.path)
+    python_libs = os.path.join(VENV_BASE, f"lib/python{sys.version_info.major}.{sys.version_info.minor}/site-packages")
+    site.addsitedir(python_libs)
+    sys.path[:] = sys.path[prev_length:] + sys.path[0:prev_length]
+    sys.real_prefix = sys.prefix
+    sys.prefix = VENV_BASE
+    #
+    print(f"VTK is using venv: {VENV_BASE}")
diff --git a/Web/Python/vtkmodules/web/vtkjs_helper.py b/Web/Python/vtkmodules/web/vtkjs_helper.py
new file mode 100644 (file)
index 0000000..4a2f6ae
--- /dev/null
@@ -0,0 +1,283 @@
+import base64
+import json
+import re
+import os
+import shutil
+import sys
+import zipfile
+
+try:
+    import zlib
+
+    compression = zipfile.ZIP_DEFLATED
+except:
+    compression = zipfile.ZIP_STORED
+
+# -----------------------------------------------------------------------------
+
+
+def convertDirectoryToZipFile(directoryPath):
+    if os.path.isfile(directoryPath):
+        return
+
+    zipFilePath = "%s.zip" % directoryPath
+    zf = zipfile.ZipFile(zipFilePath, mode="w")
+
+    try:
+        for dirName, subdirList, fileList in os.walk(directoryPath):
+            for fname in fileList:
+                fullPath = os.path.join(dirName, fname)
+                relPath = "%s" % (os.path.relpath(fullPath, directoryPath))
+                zf.write(fullPath, arcname=relPath, compress_type=compression)
+    finally:
+        zf.close()
+
+    shutil.rmtree(directoryPath)
+    shutil.move(zipFilePath, directoryPath)
+
+
+# -----------------------------------------------------------------------------
+
+
+def addDataToViewer(dataPath, srcHtmlPath, disableGirder=False):
+    if os.path.isfile(dataPath) and os.path.exists(srcHtmlPath):
+        dstDir = os.path.dirname(dataPath)
+        dstHtmlPath = os.path.join(dstDir, "%s.html" % os.path.basename(dataPath)[:-6])
+
+        # Extract data as base64
+        with open(dataPath, "rb") as data:
+            dataContent = data.read()
+            base64Content = base64.b64encode(dataContent)
+            base64Content = base64Content.decode().replace("\n", "")
+
+        # Create new output file
+        with open(srcHtmlPath, mode="r", encoding="utf-8") as srcHtml:
+            with open(dstHtmlPath, mode="w", encoding="utf-8") as dstHtml:
+                for line in srcHtml:
+                    if disableGirder and "</title>" in line:
+                        dstHtml.write(
+                            """
+    <script>
+        // Force reloading the page if we want to disable girder before anything else.
+        const urlParams = new URLSearchParams(window.location.search);
+        if (urlParams.get('noGirder') != 'true') {
+            urlParams.set('noGirder', 'true');
+            window.location.search = urlParams;
+        }
+    </script>
+                                      """
+                        )
+                    if "</body>" in line:
+                        dstHtml.write("<script>\n")
+                        dstHtml.write('var contentToLoad = "%s";\n\n' % base64Content)
+                        dstHtml.write(
+                            'Glance.importBase64Dataset("%s" , contentToLoad, glanceInstance.proxyManager);\n'
+                            % os.path.basename(dataPath)
+                        )
+                        dstHtml.write("glanceInstance.showApp();\n")
+                        dstHtml.write("</script>\n")
+
+                    dstHtml.write(line)
+
+
+# -----------------------------------------------------------------------------
+
+
+def numericSorted(l):
+    """Numerically sort a list of strings."""
+
+    # pattern to split name into numeric and non-numeric parts
+    splitter_pattern = re.compile('([0-9]+|[^0-9]+)')
+
+    def keyfunc(name):
+        """Sorting key for numeric sorting."""
+        split_name = re.findall(splitter_pattern, name)
+        # one-liner to convert numeric parts into integers
+        split_name = list(map(lambda x: int(x) if x.isdigit() else x, split_name))
+        # ensure that list begins with a string to avoid string<->int compare
+        if split_name and isinstance(split_name[0], int):
+            split_name.insert(0, '')
+        return split_name
+
+    # return the numerically sorted list
+    return sorted(l, key=keyfunc)
+
+
+# -----------------------------------------------------------------------------
+
+
+def zipAllTimeSteps(directoryPath):
+    if os.path.isfile(directoryPath):
+        return
+
+    class UrlCounterDict(dict):
+        Counter = 0
+
+        def GetUrlName(self, name):
+            if name not in self.keys():
+                self[name] = str(objNameToUrls.Counter)
+                self.Counter = self.Counter + 1
+            return self[name]
+
+    def InitIndex(sourcePath, destObj):
+        with open(sourcePath, "r") as sourceFile:
+            sourceData = sourceFile.read()
+            sourceObj = json.loads(sourceData)
+            for key in sourceObj:
+                destObj[key] = sourceObj[key]
+            # remove vtkHttpDataSetReader information
+            for obj in destObj["scene"]:
+                obj.pop(obj["type"])
+                obj.pop("type")
+
+    def getUrlToNameDictionary(indexObj):
+        urls = {}
+        for obj in indexObj["scene"]:
+            urls[obj[obj["type"]]["url"]] = obj["name"]
+        return urls
+
+    def addDirectoryToZip(
+        dirname, zipobj, storedData, rootIdx, timeStep, objNameToUrls
+    ):
+        # Update root index.json file from index.json of this timestep
+        with open(os.path.join(dirname, "index.json"), "r") as currentIdxFile:
+            currentIdx = json.loads(currentIdxFile.read())
+            urlToName = getUrlToNameDictionary(currentIdx)
+            rootTimeStepSection = rootIdx["animation"]["timeSteps"][timeStep]
+            for key in currentIdx:
+                if key == "scene" or key == "version":
+                    continue
+                rootTimeStepSection[key] = currentIdx[key]
+            for obj in currentIdx["scene"]:
+                objName = obj["name"]
+                rootTimeStepSection[objName] = {}
+                rootTimeStepSection[objName]["actor"] = obj["actor"]
+                rootTimeStepSection[objName]["actorRotation"] = obj["actorRotation"]
+                rootTimeStepSection[objName]["mapper"] = obj["mapper"]
+                rootTimeStepSection[objName]["property"] = obj["property"]
+
+        # For every object in the current timestep
+        for folder in sorted(os.listdir(dirname)):
+            currentItem = os.path.join(dirname, folder)
+            if os.path.isdir(currentItem) is False:
+                continue
+            # Write all data array of the current timestep in the archive
+            for filename in os.listdir(os.path.join(currentItem, "data")):
+                fullpath = os.path.join(currentItem, "data", filename)
+                if os.path.isfile(fullpath) and filename not in storedData:
+                    storedData.add(filename)
+                    relPath = os.path.join("data", filename)
+                    zipobj.write(fullpath, arcname=relPath, compress_type=compression)
+            # Write the index.json containing pointers to these data arrays
+            # while replacing every basepath as '../../data'
+            objIndexFilePath = os.path.join(dirname, folder, "index.json")
+            with open(objIndexFilePath, "r") as objIndexFile:
+                objIndexObjData = json.loads(objIndexFile.read())
+            for elm in objIndexObjData.keys():
+                try:
+                    if "ref" in objIndexObjData[elm].keys():
+                        objIndexObjData[elm]["ref"]["basepath"] = "../../data"
+                    if "arrays" in objIndexObjData[elm].keys():
+                        for array in objIndexObjData[elm]["arrays"]:
+                            array["data"]["ref"]["basepath"] = "../../data"
+                except AttributeError:
+                    continue
+            currentObjName = urlToName[folder]
+            objIndexRelPath = os.path.join(
+                objNameToUrls.GetUrlName(currentObjName), str(timeStep), "index.json"
+            )
+            zipobj.writestr(
+                objIndexRelPath,
+                json.dumps(objIndexObjData, indent=2),
+                compress_type=compression,
+            )
+
+    # ---
+
+    zipFilePath = "%s.zip" % directoryPath
+    currentDirectory = os.path.abspath(os.path.join(directoryPath, os.pardir))
+    rootIndexPath = os.path.join(currentDirectory, "index.json")
+    rootIndexFile = open(rootIndexPath, "r")
+    rootIndexObj = json.loads(rootIndexFile.read())
+
+    zf = zipfile.ZipFile(zipFilePath, mode="w")
+    try:
+        # We copy the scene from an index of a specific timestep to the root index
+        # Scenes should all have the same objects so only do it for the first one
+        isSceneInitialized = False
+        # currentlyAddedData set stores hashes of every data we already added to the
+        # vtkjs archive to prevent data duplication
+        currentlyAddedData = set()
+        # Regex that folders storing timestep data from paraview should follow
+        reg = re.compile(r"^" + os.path.basename(directoryPath) + r"\.[0-9]+$")
+        # We assume an object will not be deleted from a timestep to another so we create a generic index.json for each object
+        genericIndexObj = {}
+        genericIndexObj["series"] = []
+        timeStep = 0
+        for item in rootIndexObj["animation"]["timeSteps"]:
+            genericIndexObj["series"].append({})
+            genericIndexObj["series"][timeStep]["url"] = str(timeStep)
+            genericIndexObj["series"][timeStep]["timeStep"] = float(item["time"])
+            timeStep = timeStep + 1
+        # Keep track of the url for every object
+        objNameToUrls = UrlCounterDict()
+
+        timeStep = 0
+        # zip all timestep directories
+        for folder in numericSorted(os.listdir(currentDirectory)):
+            fullPath = os.path.join(currentDirectory, folder)
+            if os.path.isdir(fullPath) and reg.match(folder):
+                if not isSceneInitialized:
+                    InitIndex(os.path.join(fullPath, "index.json"), rootIndexObj)
+                    isSceneInitialized = True
+                addDirectoryToZip(
+                    fullPath,
+                    zf,
+                    currentlyAddedData,
+                    rootIndexObj,
+                    timeStep,
+                    objNameToUrls,
+                )
+                shutil.rmtree(fullPath)
+                timeStep = timeStep + 1
+
+        # Write every index.json holding time information for each object
+        for name in objNameToUrls:
+            zf.writestr(
+                os.path.join(objNameToUrls[name], "index.json"),
+                json.dumps(genericIndexObj, indent=2),
+                compress_type=compression,
+            )
+
+        # Update root index.json urls and write it in the archive
+        for obj in rootIndexObj["scene"]:
+            obj["id"] = obj["name"]
+            obj["type"] = "vtkHttpDataSetSeriesReader"
+            obj["vtkHttpDataSetSeriesReader"] = {}
+            obj["vtkHttpDataSetSeriesReader"]["url"] = objNameToUrls[obj["name"]]
+        zf.writestr(
+            "index.json", json.dumps(rootIndexObj, indent=2), compress_type=compression
+        )
+        os.remove(rootIndexPath)
+
+    finally:
+        zf.close()
+
+    shutil.move(zipFilePath, directoryPath)
+
+
+# -----------------------------------------------------------------------------
+# Main
+# -----------------------------------------------------------------------------
+
+if __name__ == "__main__":
+    if len(sys.argv) < 2:
+        print(
+            "Usage: directoryToFile /path/to/directory.vtkjs [/path/to/ParaViewGlance.html]"
+        )
+    else:
+        fileName = sys.argv[1]
+        convertDirectoryToZipFile(fileName)
+
+        if len(sys.argv) == 3:
+            addDataToViewer(fileName, sys.argv[2])
diff --git a/Web/Python/vtkmodules/web/wslink.py b/Web/Python/vtkmodules/web/wslink.py
new file mode 100644 (file)
index 0000000..20cf68e
--- /dev/null
@@ -0,0 +1,67 @@
+r"""wslink is a module that extends any
+wslink related classes for the purposes of vtkWeb.
+
+"""
+
+from __future__ import absolute_import, division, print_function
+
+# import inspect, types, string, random, logging, six, json, re, base64
+import json, base64, logging, time
+
+from vtkmodules.web.errors import WebDependencyMissingError
+
+try:
+    from wslink import websocket
+    from wslink import register as exportRpc
+except ImportError:
+    raise WebDependencyMissingError()
+
+from vtkmodules.web import protocols
+from vtkmodules.vtkWebCore import vtkWebApplication
+
+# =============================================================================
+application = None
+
+# =============================================================================
+#
+# Base class for vtkWeb ServerProtocol
+#
+# =============================================================================
+
+
+class ServerProtocol(websocket.ServerProtocol):
+    """
+    Defines the core server protocol for vtkWeb. Adds support to
+    marshall/unmarshall RPC callbacks that involve ServerManager proxies as
+    arguments or return values.
+
+    Applications typically don't use this class directly, but instead
+    sub-class it and call self.registerVtkWebProtocol() with useful vtkWebProtocols.
+    """
+
+    def __init__(self):
+        logging.info("Creating SP")
+        self.setSharedObject("app", self.initApplication())
+        websocket.ServerProtocol.__init__(self)
+
+    def initApplication(self):
+        """
+        Let subclass optionally initialize a custom application in lieu
+        of the default vtkWebApplication.
+        """
+        global application
+        if not application:
+            application = vtkWebApplication()
+        return application
+
+    def setApplication(self, application):
+        self.setSharedObject("app", application)
+
+    def getApplication(self):
+        return self.getSharedObject("app")
+
+    def registerVtkWebProtocol(self, protocol):
+        self.registerLinkProtocol(protocol)
+
+    def getVtkWebProtocols(self):
+        return self.getLinkProtocols()
diff --git a/Web/WebAssembly/CMakeLists.txt b/Web/WebAssembly/CMakeLists.txt
new file mode 100644 (file)
index 0000000..9df8571
--- /dev/null
@@ -0,0 +1,128 @@
+if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+  message(FATAL_ERROR
+    "The VTK::WebAssembly module requires Emscripten compiler.")
+endif ()
+
+set(classes
+  vtkWasmSceneManager)
+
+vtk_module_add_module(VTK::WebAssembly
+  CLASSES ${classes})
+
+vtk_add_test_mangling(VTK::WebAssembly)
+
+set(_vtk_wasm_scene_manager_autoinit_mods)
+get_property(_vtk_wasm_scene_manager_optional_deps GLOBAL
+  PROPERTY "_vtk_module_VTK::WebAssembly_optional_depends")
+foreach(_module IN LISTS _vtk_wasm_scene_manager_private_deps _vtk_wasm_scene_manager_optional_deps)
+  if (NOT TARGET "${_module}")
+    continue ()
+  endif ()
+  list(APPEND _vtk_wasm_scene_manager_autoinit_mods "${_module}")
+endforeach()
+vtk_module_autoinit(
+  TARGETS WebAssembly
+  MODULES ${_vtk_wasm_scene_manager_autoinit_mods})
+# -----------------------------------------------------------------------------
+# Emscripten compile+link options
+# -----------------------------------------------------------------------------
+set(emscripten_link_options)
+list(APPEND emscripten_link_options
+  "-lembind"
+  "--extern-post-js=${CMAKE_CURRENT_SOURCE_DIR}/post.js"
+  # "--embind-emit-tsd=vtkWasmSceneManager.ts"
+  #"--memoryprofiler"
+  #"--cpuprofiler"
+  "-sALLOW_MEMORY_GROWTH=1"
+  "-sALLOW_TABLE_GROWTH=1"
+  "-sEXPORT_NAME=vtkWasmSceneManager"
+  "-sENVIRONMENT=node,web"
+  "-sEXPORTED_RUNTIME_METHODS=['addFunction','UTF8ToString','FS']"
+  # "-sEXCEPTION_DEBUG=1" # prints stack trace for uncaught C++ exceptions from VTK (very rare, but PITA to figure out)
+  # "-sGL_DEBUG=1"
+  # "-sGL_ASSERTIONS=1"
+  # "-sTRACE_WEBGL_CALLS=1"
+  )
+if (CMAKE_SIZEOF_VOID_P EQUAL "8")
+  list(APPEND emscripten_link_options
+    "-sMAXIMUM_MEMORY=16GB")
+else ()
+  list(APPEND emscripten_link_options
+    "-sMAXIMUM_MEMORY=4GB")
+endif ()
+# -----------------------------------------------------------------------------
+# Optimizations
+# -----------------------------------------------------------------------------
+set(emscripten_optimizations)
+set(emscripten_debug_options)
+if (CMAKE_BUILD_TYPE STREQUAL "Release")
+  set(vtk_scene_manager_wasm_optimize "BEST")
+  set(vtk_scene_manager_wasm_debuginfo "NONE")
+elseif (CMAKE_BUILD_TYPE STREQUAL "MinSizeRel")
+  set(vtk_scene_manager_wasm_optimize "SMALLEST_WITH_CLOSURE")
+  set(vtk_scene_manager_wasm_debuginfo "NONE")
+elseif (CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
+  set(vtk_scene_manager_wasm_optimize "MORE")
+  set(vtk_scene_manager_wasm_debuginfo "PROFILE")
+elseif (CMAKE_BUILD_TYPE STREQUAL "Debug")
+  set(vtk_scene_manager_wasm_optimize "NO_OPTIMIZATION")
+  set(vtk_scene_manager_wasm_debuginfo "DEBUG_NATIVE")
+endif ()
+set(vtk_scene_manager_wasm_optimize_NO_OPTIMIZATION "-O0")
+set(vtk_scene_manager_wasm_optimize_LITTLE "-O1")
+set(vtk_scene_manager_wasm_optimize_MORE "-O2")
+set(vtk_scene_manager_wasm_optimize_BEST "-O3")
+set(vtk_scene_manager_wasm_optimize_SMALLEST "-Os")
+set(vtk_scene_manager_wasm_optimize_SMALLEST_WITH_CLOSURE "-Oz")
+set(vtk_scene_manager_wasm_optimize_SMALLEST_WITH_CLOSURE_link "--closure=1")
+
+if (DEFINED "vtk_scene_manager_wasm_optimize_${vtk_scene_manager_wasm_optimize}")
+  list(APPEND emscripten_optimizations
+    ${vtk_scene_manager_wasm_optimize_${vtk_scene_manager_wasm_optimize}})
+  list(APPEND emscripten_link_options
+    ${vtk_scene_manager_wasm_optimize_${vtk_scene_manager_wasm_optimize}_link})
+else ()
+  message (FATAL_ERROR "Unrecognized value for vtk_scene_manager_wasm_optimize=${vtk_scene_manager_wasm_optimize}")
+endif ()
+
+set(vtk_scene_manager_wasm_debuginfo_NONE "-g0")
+set(vtk_scene_manager_wasm_debuginfo_READABLE_JS "-g1")
+set(vtk_scene_manager_wasm_debuginfo_PROFILE "-g2")
+set(vtk_scene_manager_wasm_debuginfo_DEBUG_NATIVE "-g3")
+set(vtk_scene_manager_wasm_debuginfo_DEBUG_NATIVE_link "-sASSERTIONS=1")
+if (DEFINED "vtk_scene_manager_wasm_debuginfo_${vtk_scene_manager_wasm_debuginfo}")
+  list(APPEND emscripten_debug_options
+    ${vtk_scene_manager_wasm_debuginfo_${vtk_scene_manager_wasm_debuginfo}})
+  list(APPEND emscripten_link_options
+    ${vtk_scene_manager_wasm_debuginfo_${vtk_scene_manager_wasm_debuginfo}_link})
+else ()
+  message (FATAL_ERROR "Unrecognized value for vtk_scene_manager_wasm_debuginfo=${vtk_scene_manager_wasm_debuginfo}")
+endif ()
+
+vtk_module_add_executable(WasmSceneManager
+  BASENAME vtkWasmSceneManager
+  vtkWasmSceneManagerEmBinding.cxx)
+add_executable("VTK::WasmSceneManager" ALIAS
+  WasmSceneManager)
+target_link_libraries(WasmSceneManager
+  PRIVATE
+    VTK::WebAssembly)
+target_compile_options(WasmSceneManager
+  PRIVATE
+    ${emscripten_compile_options}
+    ${emscripten_optimizations}
+    ${emscripten_debug_options})
+target_link_options(WasmSceneManager
+  PRIVATE
+    ${emscripten_link_options}
+    ${emscripten_optimizations}
+    ${emscripten_debug_options})
+set_target_properties(WasmSceneManager
+  PROPERTIES
+  OUTPUT_NAME "vtkWasmSceneManager"
+  SUFFIX ".mjs")
+# [cmake/cmake#20745](https://gitlab.kitware.com/cmake/cmake/-/issues/20745)
+# CMake doesn't install multiple files associated with an executable target.
+install(FILES
+  "$<TARGET_FILE_DIR:WasmSceneManager>/vtkWasmSceneManager.wasm"
+  DESTINATION ${CMAKE_INSTALL_BINDIR})
diff --git a/Web/WebAssembly/Testing/CMakeLists.txt b/Web/WebAssembly/Testing/CMakeLists.txt
new file mode 100644 (file)
index 0000000..a82329d
--- /dev/null
@@ -0,0 +1,8 @@
+vtk_module_test_data(
+  Data/WasmSceneManager/scalar-bar-widget.blobs.json
+  Data/WasmSceneManager/scalar-bar-widget.states.json
+  Data/WasmSceneManager/simple.blobs.json)
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+  add_subdirectory(JavaScript)
+endif ()
diff --git a/Web/WebAssembly/Testing/JavaScript/CMakeLists.txt b/Web/WebAssembly/Testing/JavaScript/CMakeLists.txt
new file mode 100644 (file)
index 0000000..1a13dd7
--- /dev/null
@@ -0,0 +1,22 @@
+set(vtk_nodejs_min_version "23.8.0")
+find_package(NodeJS "${vtk_nodejs_min_version}" REQUIRED)
+if (VTK_WEBASSEMBLY_64_BIT)
+  set(_vtk_node_args "--experimental-wasm-memory64")
+endif ()
+set(_vtk_testing_nodejs_exe "${NodeJS_INTERPRETER}")
+
+if (CMAKE_HOST_WIN32)
+  list(APPEND _vtk_node_args
+    --import "file://$<TARGET_FILE:VTK::WasmSceneManager>")
+else ()
+  list(APPEND _vtk_node_args
+    --import "$<TARGET_FILE:VTK::WasmSceneManager>")
+endif ()
+vtk_add_test_module_javascript_node(
+  testBindRenderWindow.mjs
+  testBlobs.mjs
+  testInitialize.mjs,NO_DATA
+  testInvoke.mjs
+  testOSMesaRenderWindowPatch.mjs
+  testSkipProperty.mjs
+  testStates.mjs)
diff --git a/Web/WebAssembly/Testing/JavaScript/testBindRenderWindow.mjs b/Web/WebAssembly/Testing/JavaScript/testBindRenderWindow.mjs
new file mode 100644 (file)
index 0000000..3c2d57f
--- /dev/null
@@ -0,0 +1,53 @@
+async function testBindRenderWindow() {
+    const manager = await globalThis.createVTKWasmSceneManager({});
+    manager.initialize();
+    manager.registerStateJSON(
+        {
+            Id: 1,
+            ClassName: "vtkCocoaRenderWindow",
+            SuperClassNames: ["vtkRenderWindow"],
+            Interactor: { Id: 2 },
+            "vtk-object-manager-kept-alive": true,
+        });
+    manager.registerStateJSON(
+        {
+            Id: 2,
+            ClassName: "vtkCocoaRenderWindowInteractor",
+            SuperClassNames: ["vtkRenderWindowInteractor"],
+            RenderWindow: { Id: 1 },
+        });
+    manager.updateObjectsFromStates();
+
+    manager.bindRenderWindow(1, "#my-canvas-id");
+
+    manager.updateStateFromObject(1);
+    if (manager.getState(1).CanvasSelector !== "#my-canvas-id") {
+        throw new Error("CanvasSelector was not set correctly on RenderWindow.");
+    }
+
+    manager.updateStateFromObject(2);
+    if (manager.getState(2).CanvasSelector !== "#my-canvas-id") {
+        throw new Error("CanvasSelector was not set correctly on RenderWindowInteractor.");
+    }
+}
+const tests = [
+    {
+        description: "Bind RenderWindow to Canvas",
+        test: testBindRenderWindow,
+    },
+];
+
+let exitCode = 0;
+for (let test of tests) {
+    try {
+        await test.test();
+        console.log("✓", test.description);
+        exitCode |= 0;
+    }
+    catch (error) {
+        console.log("x", test.description);
+        console.log(error);
+        exitCode |= 1;
+    }
+}
+process.exit(exitCode);
diff --git a/Web/WebAssembly/Testing/JavaScript/testBlobs.mjs b/Web/WebAssembly/Testing/JavaScript/testBlobs.mjs
new file mode 100644 (file)
index 0000000..a1ddaf4
--- /dev/null
@@ -0,0 +1,52 @@
+import { readFile } from "fs/promises";
+import path from "path";
+
+async function testBlobs() {
+  const dataDirectoryIndex = process.argv.indexOf("-D") + 1;
+  if (dataDirectoryIndex <= 0) {
+    throw new Error("Please provide path to a blobs file using -D");
+  }
+  const dataDirectory = process.argv[dataDirectoryIndex];
+  const blobs = JSON.parse(await readFile(path.join(dataDirectory, "Data", "WasmSceneManager", "simple.blobs.json")));
+  const manager = await globalThis.createVTKWasmSceneManager({})
+  if (!manager.initialize()) {
+    throw new Error("Failed to initialize scene manager");
+  }
+
+  for (let hash in blobs) {
+    if (!manager.registerBlob(hash, new Uint8Array(blobs[hash].bytes))) {
+      throw new Error(`Failed to register blob with hash=${hash}`);
+    }
+  }
+  for (let hash in blobs) {
+    const blob = manager.getBlob(hash);
+    if (!(blob instanceof Uint8Array)) {
+      throw new Error(`getBlob did not return a Uint8Array for hash=${hash}`);
+    }
+    if (blob.toString() !== blobs[hash].bytes.toString()) {
+      throw new Error(`blob for hash=${hash} does not match registered blob.`);
+    }
+  }
+}
+
+const tests = [
+  {
+    description: "Register blobs with hashes",
+    test: testBlobs,
+  },
+];
+
+let exitCode = 0;
+for (let test of tests) {
+  try {
+    await test.test();
+    console.log("✓", test.description);
+    exitCode |= 0;
+  }
+  catch (error) {
+    console.log("x", test.description);
+    console.log(error);
+    exitCode |= 1;
+  }
+}
+process.exit(exitCode);
diff --git a/Web/WebAssembly/Testing/JavaScript/testInitialize.mjs b/Web/WebAssembly/Testing/JavaScript/testInitialize.mjs
new file mode 100644 (file)
index 0000000..acdae02
--- /dev/null
@@ -0,0 +1,27 @@
+async function testInitialize() {
+  const manager = await globalThis.createVTKWasmSceneManager({});
+  if (!manager.initialize()) {
+    throw new Error();
+  }
+}
+const tests = [
+  {
+    description: "Initialize VTK scene manager",
+    test: testInitialize,
+  },
+];
+
+let exitCode = 0;
+for (let test of tests) {
+  try {
+    await test.test();
+    console.log("✓", test.description);
+    exitCode |= 0;
+  }
+  catch (error) {
+    console.log("x", test.description);
+    console.log(error);
+    exitCode |= 1;
+  }
+}
+process.exit(exitCode);
diff --git a/Web/WebAssembly/Testing/JavaScript/testInvoke.mjs b/Web/WebAssembly/Testing/JavaScript/testInvoke.mjs
new file mode 100644 (file)
index 0000000..afdf321
--- /dev/null
@@ -0,0 +1,49 @@
+async function testInvoke() {
+  const manager = await globalThis.createVTKWasmSceneManager({});
+  manager.initialize();
+  // manager.setDeserializerLogVerbosity("INFO");
+  // manager.setObjectManagerLogVerbosity("INFO");
+  // manager.setInvokerLogVerbosity("INFO");
+  manager.registerStateJSON({
+    "ClassName": "vtkCamera", "SuperClassNames": ["vtkObject"], "vtk-object-manager-kept-alive": true, "Id": 1
+  });
+
+  manager.updateObjectsFromStates();
+
+  manager.updateStateFromObject(1);
+  let state = manager.getState(1);
+  if (JSON.stringify(state.Position) != JSON.stringify([0, 0, 1])) {
+    throw new Error("Failed to initialize camera state");
+  }
+
+  // Invoke a method named "Elevation" on the camera with argument 10.0
+  manager.invoke(1, "Elevation", [10.0]);
+
+  manager.updateStateFromObject(1);
+  state = manager.getState(1);
+  if (JSON.stringify(state.Position) != JSON.stringify([0, 0.17364817766693033, 0.9848077530122081])) {
+    throw new Error("vtkCamera::Elevation(10) did not work!");
+  }
+}
+
+const tests = [
+  {
+    description: "Invoke methods",
+    test: testInvoke,
+  },
+];
+
+let exitCode = 0;
+for (let test of tests) {
+  try {
+    await test.test();
+    console.log("✓", test.description);
+    exitCode |= 0;
+  }
+  catch (error) {
+    console.log("x", test.description);
+    console.log(error);
+    exitCode |= 1;
+  }
+}
+process.exit(exitCode);
diff --git a/Web/WebAssembly/Testing/JavaScript/testOSMesaRenderWindowPatch.mjs b/Web/WebAssembly/Testing/JavaScript/testOSMesaRenderWindowPatch.mjs
new file mode 100644 (file)
index 0000000..4a1597a
--- /dev/null
@@ -0,0 +1,45 @@
+async function testOSMesaRenderWindowPatch() {
+    const manager = await globalThis.createVTKWasmSceneManager({});
+    manager.initialize();
+    manager.registerStateJSON({
+        Id: 1,
+        ClassName: "vtkOSOpenGLRenderWindow",
+        SuperClassNames: ["vtkWindow", "vtkRenderWindow"],
+        "vtk-object-manager-kept-alive": true,
+    });
+    if (manager.getState(1).ClassName !== "vtkWebAssemblyOpenGLRenderWindow") {
+        throw new Error("RenderWindow state was not created as vtkWebAssemblyOpenGLRenderWindow.");
+    }
+    manager.updateObjectsFromStates();
+
+    manager.updateObjectFromStateJSON({
+        Id: 1,
+        ClassName: "vtkOSOpenGLRenderWindow",
+        SuperClassNames: ["vtkWindow", "vtkRenderWindow"],
+        "vtk-object-manager-kept-alive": true,
+    });
+    if (manager.getState(1).ClassName !== "vtkWebAssemblyOpenGLRenderWindow") {
+        throw new Error("RenderWindow state was not updated as vtkWebAssemblyOpenGLRenderWindow.");
+    }
+}
+const tests = [
+    {
+        description: "Patch vtkOSOpenGLRenderWindow to vtkWebAssemblyOpenGLRenderWindow",
+        test: testOSMesaRenderWindowPatch,
+    },
+];
+
+let exitCode = 0;
+for (let test of tests) {
+    try {
+        await test.test();
+        console.log("✓", test.description);
+        exitCode |= 0;
+    }
+    catch (error) {
+        console.log("x", test.description);
+        console.log(error);
+        exitCode |= 1;
+    }
+}
+process.exit(exitCode);
diff --git a/Web/WebAssembly/Testing/JavaScript/testSkipProperty.mjs b/Web/WebAssembly/Testing/JavaScript/testSkipProperty.mjs
new file mode 100644 (file)
index 0000000..9b39aa3
--- /dev/null
@@ -0,0 +1,59 @@
+async function testSkipProperty() {
+    const manager = await globalThis.createVTKWasmSceneManager({});
+    manager.initialize();
+    manager.registerStateJSON({
+        "ClassName": "vtkCamera", "SuperClassNames": ["vtkObject"], "vtk-object-manager-kept-alive": true, "Id": 1
+    });
+
+    manager.updateObjectsFromStates();
+
+    // Skip Position and update object.
+    manager.skipProperty("vtkOpenGLCamera", "Position");
+    manager.updateObjectFromStateJSON({
+        "Id": 1,
+        "Position": [0, 1, 2]
+    });
+
+    // Verify property was skipped
+    manager.updateStateFromObject(1);
+    let state = manager.getState(1);
+    if (JSON.stringify(state.Position) == JSON.stringify([0, 1, 2])) {
+        throw new Error("vtkCamera::Position did not get skipped!");
+    }
+
+    // UnSkip Position and update object.
+    manager.unSkipProperty("vtkOpenGLCamera", "Position");
+    manager.updateObjectFromStateJSON({
+        "Id": 1,
+        "Position": [3, 4, 5]
+    });
+
+    // Verify property was deserialized
+    manager.updateStateFromObject(1);
+    state = manager.getState(1);
+    if (JSON.stringify(state.Position) != JSON.stringify([3, 4, 5])) {
+        throw new Error("vtkCamera::Position did not get unskipped!");
+    }
+}
+
+const tests = [
+    {
+        description: "Invoke methods",
+        test: testSkipProperty,
+    },
+];
+
+let exitCode = 0;
+for (let test of tests) {
+    try {
+        await test.test();
+        console.log("✓", test.description);
+        exitCode |= 0;
+    }
+    catch (error) {
+        console.log("x", test.description);
+        console.log(error);
+        exitCode |= 1;
+    }
+}
+process.exit(exitCode);
diff --git a/Web/WebAssembly/Testing/JavaScript/testStates.mjs b/Web/WebAssembly/Testing/JavaScript/testStates.mjs
new file mode 100644 (file)
index 0000000..b9219d8
--- /dev/null
@@ -0,0 +1,60 @@
+import { readFile } from "fs/promises";
+import path from "path";
+
+const object_ids = [1, 2, 3, 41, 5, 42, 44, 4, 6, 33, 35, 38, 40, 43, 11, 45, 46, 47, 48, 49, 50, 51, 7, 34, 36, 37, 39, 12, 8, 9, 10, 13, 14, 15, 16, 19, 21, 24, 27, 30, 17, 18, 20, 22, 23, 25, 26, 28, 29, 31, 32]
+const exepected_dependencies = [1, 2, 3, 41, 5, 42, 44, 4, 6, 33, 35, 38, 40, 43, 11, 45, 46, 47, 48, 49, 50, 51, 7, 34, 36, 37, 39, 12, 8, 9, 10, 13, 14, 15, 16, 19, 21, 24, 27, 30, 17, 18, 20, 22, 23, 25, 26, 28, 29]
+
+async function testStates() {
+  const dataDirectoryIndex = process.argv.indexOf("-D") + 1;
+  if (dataDirectoryIndex <= 0) {
+    throw new Error("Please provide path to a blobs file using -D");
+  }
+  const dataDirectory = process.argv[dataDirectoryIndex];
+  const blobs = JSON.parse(await readFile(path.join(dataDirectory, "Data", "WasmSceneManager", "scalar-bar-widget.blobs.json")));
+  const states = JSON.parse(await readFile(path.join(dataDirectory, "Data", "WasmSceneManager", "scalar-bar-widget.states.json")));
+  const manager = await globalThis.createVTKWasmSceneManager({});
+  if (!manager.initialize()) {
+    throw new Error("Failed to initialize scene manager");
+  }
+  for (let i = 0; i < object_ids.length; ++i) {
+    const object_id = object_ids[i];
+    if (!manager.registerState(JSON.stringify(states[object_id]))) {
+      throw new Error(`Failed to register state at object_id=${object_id}`);
+    }
+  }
+  for (let hash in blobs) {
+    if (!manager.registerBlob(hash, new Uint8Array(blobs[hash].bytes))) {
+      throw new Error(`Failed to register blob with hash=${hash}`);
+    }
+  }
+  manager.updateObjectsFromStates();
+  const activeIds = manager.getAllDependencies(0);
+  if (!(activeIds instanceof Uint32Array)) {
+    throw new Error("getAllDependencies did not return a Uint32Array");
+  }
+  if (activeIds.toString() != exepected_dependencies.toString()) {
+    throw new Error(`${activeIds} != ${exepected_dependencies}`);
+  }
+}
+
+const tests = [
+  {
+    description: "Register states",
+    test: testStates,
+  },
+];
+
+let exitCode = 0;
+for (let test of tests) {
+  try {
+    await test.test();
+    console.log("✓", test.description);
+    exitCode |= 0;
+  }
+  catch (error) {
+    console.log("x", test.description);
+    console.log(error);
+    exitCode |= 1;
+  }
+}
+process.exit(exitCode);
diff --git a/Web/WebAssembly/post.js b/Web/WebAssembly/post.js
new file mode 100644 (file)
index 0000000..c134433
--- /dev/null
@@ -0,0 +1 @@
+globalThis.createVTKWasmSceneManager = vtkWasmSceneManager;
diff --git a/Web/WebAssembly/vtk.module b/Web/WebAssembly/vtk.module
new file mode 100644 (file)
index 0000000..bf2a3ff
--- /dev/null
@@ -0,0 +1,17 @@
+NAME
+  VTK::WebAssembly
+LIBRARY_NAME
+  vtkWebAssembly
+SPDX_LICENSE_IDENTIFIER
+  BSD-3-Clause
+SPDX_COPYRIGHT_TEXT
+  Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+DEPENDS
+  VTK::SerializationManager
+PRIVATE_DEPENDS
+  VTK::RenderingCore
+OPTIONAL_DEPENDS
+  VTK::RenderingContextOpenGL2
+  VTK::RenderingOpenGL2
+  VTK::RenderingUI
+  VTK::RenderingVolumeOpenGL2
diff --git a/Web/WebAssembly/vtkWasmSceneManager.cxx b/Web/WebAssembly/vtkWasmSceneManager.cxx
new file mode 100644 (file)
index 0000000..41858ee
--- /dev/null
@@ -0,0 +1,232 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+#include "vtkWasmSceneManager.h"
+
+#include "vtkCallbackCommand.h"
+#include "vtkCommand.h"
+#include "vtkObjectFactory.h"
+
+#include "vtkRenderWindow.h"
+#include "vtkRenderWindowInteractor.h"
+#include "vtkRenderer.h"
+#include "vtkWebAssemblyRenderWindowInteractor.h"
+
+// Init factories.
+#ifdef VTK_MODULE_ENABLE_VTK_RenderingContextOpenGL2
+#include "vtkRenderingContextOpenGL2Module.h"
+#endif
+#ifdef VTK_MODULE_ENABLE_VTK_RenderingOpenGL2
+#include "vtkOpenGLPolyDataMapper.h" // needed to remove unused mapper, also includes vtkRenderingOpenGL2Module.h
+#include "vtkWebAssemblyOpenGLRenderWindow.h"
+#endif
+#ifdef VTK_MODULE_ENABLE_VTK_RenderingUI
+#include "vtkRenderingUIModule.h"
+#endif
+#ifdef VTK_MODULE_ENABLE_VTK_RenderingVolumeOpenGL2
+#include "vtkRenderingVolumeOpenGL2Module.h"
+#endif
+
+VTK_ABI_NAMESPACE_BEGIN
+//-------------------------------------------------------------------------------
+vtkStandardNewMacro(vtkWasmSceneManager);
+
+//-------------------------------------------------------------------------------
+vtkWasmSceneManager::vtkWasmSceneManager() = default;
+
+//-------------------------------------------------------------------------------
+vtkWasmSceneManager::~vtkWasmSceneManager() = default;
+
+//-------------------------------------------------------------------------------
+bool vtkWasmSceneManager::Initialize()
+{
+  bool result = this->Superclass::Initialize();
+#ifdef VTK_MODULE_ENABLE_VTK_RenderingOpenGL2
+  // Remove the default vtkOpenGLPolyDataMapper as it is not used with wasm build.
+  /// get rid of serialization handler
+  this->Serializer->UnRegisterHandler(typeid(vtkOpenGLPolyDataMapper));
+  /// get rid of de-serialization handler
+  this->Deserializer->UnRegisterHandler(typeid(vtkOpenGLPolyDataMapper));
+  /// get rid of constructor
+  this->Deserializer->UnRegisterConstructor("vtkOpenGLPolyDataMapper");
+#endif
+  return result;
+}
+
+//-------------------------------------------------------------------------------
+void vtkWasmSceneManager::PrintSelf(ostream& os, vtkIndent indent)
+{
+  this->vtkObjectManager::PrintSelf(os, indent);
+}
+
+//-------------------------------------------------------------------------------
+bool vtkWasmSceneManager::SetSize(vtkTypeUInt32 identifier, int width, int height)
+{
+  auto object = this->GetObjectAtId(identifier);
+  if (auto renderWindow = vtkRenderWindow::SafeDownCast(object))
+  {
+    if (auto iren = renderWindow->GetInteractor())
+    {
+      iren->UpdateSize(width, height);
+      return true;
+    }
+  }
+  return false;
+}
+
+//-------------------------------------------------------------------------------
+bool vtkWasmSceneManager::Render(vtkTypeUInt32 identifier)
+{
+  auto object = this->GetObjectAtId(identifier);
+  if (auto renderWindow = vtkRenderWindow::SafeDownCast(object))
+  {
+    renderWindow->Render();
+    return true;
+  }
+  return false;
+}
+
+//-------------------------------------------------------------------------------
+bool vtkWasmSceneManager::ResetCamera(vtkTypeUInt32 identifier)
+{
+  auto object = this->GetObjectAtId(identifier);
+  if (auto renderer = vtkRenderer::SafeDownCast(object))
+  {
+    renderer->ResetCamera();
+    return true;
+  }
+  return false;
+}
+
+//-------------------------------------------------------------------------------
+bool vtkWasmSceneManager::StartEventLoop(vtkTypeUInt32 identifier)
+{
+  vtkRenderWindowInteractor::InteractorManagesTheEventLoop = false;
+  auto object = this->GetObjectAtId(identifier);
+  if (auto* renderWindow = vtkRenderWindow::SafeDownCast(object))
+  {
+    if (auto* interactor =
+          vtkWebAssemblyRenderWindowInteractor::SafeDownCast(renderWindow->GetInteractor()))
+    {
+      if (auto* wasmGLWindow = vtkWebAssemblyOpenGLRenderWindow::SafeDownCast(renderWindow))
+      {
+        // copy canvas selector from the render window to the interactor.
+        interactor->SetCanvasSelector(wasmGLWindow->GetCanvasSelector());
+        std::cout << "Started event loop id=" << identifier
+                  << ", interactor=" << interactor->GetObjectDescription() << '\n';
+        interactor->Start();
+        return true;
+      }
+      else
+      {
+        std::cerr << "Render window class " << renderWindow->GetClassName()
+                  << " is not recognized!\n";
+      }
+    }
+    else
+    {
+      std::cerr << "Interactor class " << renderWindow->GetClassName() << " is not recognized!\n";
+    }
+  }
+  return false;
+}
+
+//-------------------------------------------------------------------------------
+bool vtkWasmSceneManager::StopEventLoop(vtkTypeUInt32 identifier)
+{
+  auto object = this->GetObjectAtId(identifier);
+  if (auto renderWindow = vtkRenderWindow::SafeDownCast(object))
+  {
+    auto interactor = renderWindow->GetInteractor();
+    std::cout << "Stopping event loop id=" << identifier
+              << ", interactor=" << interactor->GetObjectDescription() << '\n';
+    interactor->TerminateApp();
+    return true;
+  }
+  return false;
+}
+
+namespace
+{
+struct CallbackBridge
+{
+  vtkWasmSceneManager::ObserverCallbackF f;
+  vtkTypeUInt32 SenderId;
+};
+}
+
+//-------------------------------------------------------------------------------
+unsigned long vtkWasmSceneManager::AddObserver(
+  vtkTypeUInt32 identifier, std::string eventName, ObserverCallbackF callback)
+{
+  auto object = vtkObject::SafeDownCast(this->GetObjectAtId(identifier));
+  if (object == nullptr)
+  {
+    return 0;
+  }
+  vtkNew<vtkCallbackCommand> callbackCmd;
+  callbackCmd->SetClientData(new CallbackBridge{ callback, identifier });
+  callbackCmd->SetClientDataDeleteCallback(
+    [](void* clientData)
+    {
+      auto* bridge = reinterpret_cast<CallbackBridge*>(clientData);
+      delete bridge;
+    });
+  callbackCmd->SetCallback(
+    [](vtkObject*, unsigned long eid, void* clientData, void*)
+    {
+      auto* bridge = reinterpret_cast<CallbackBridge*>(clientData);
+      bridge->f(bridge->SenderId, vtkCommand::GetStringFromEventId(eid));
+    });
+  return object->AddObserver(eventName.c_str(), callbackCmd);
+}
+
+//-------------------------------------------------------------------------------
+bool vtkWasmSceneManager::RemoveObserver(vtkTypeUInt32 identifier, unsigned long tag)
+{
+
+  auto object = vtkObject::SafeDownCast(this->GetObjectAtId(identifier));
+  if (object == nullptr)
+  {
+    return false;
+  }
+  object->RemoveObserver(tag);
+  return true;
+}
+
+bool vtkWasmSceneManager::BindRenderWindow(
+  vtkTypeUInt32 renderWindowIdentifier, const char* canvasSelector)
+{
+  if (auto* renderWindow =
+        vtkRenderWindow::SafeDownCast(this->GetObjectAtId(renderWindowIdentifier)))
+  {
+    if (auto* wasmGLWindow = vtkWebAssemblyOpenGLRenderWindow::SafeDownCast(renderWindow))
+    {
+      wasmGLWindow->SetCanvasSelector(canvasSelector);
+      if (auto* interactor =
+            vtkWebAssemblyRenderWindowInteractor::SafeDownCast(renderWindow->GetInteractor()))
+      {
+        interactor->SetCanvasSelector(canvasSelector);
+        return true;
+      }
+      else
+      {
+        std::cerr << "No interactor found for render window with identifier: "
+                  << renderWindowIdentifier << '\n';
+        return false;
+      }
+    }
+    else
+    {
+      std::cerr << "Render window class " << renderWindow->GetClassName()
+                << " is not recognized!\n";
+      return false;
+    }
+  }
+  else
+  {
+    std::cerr << "No render window found with identifier: " << renderWindowIdentifier << '\n';
+    return false;
+  }
+}
+
+VTK_ABI_NAMESPACE_END
diff --git a/Web/WebAssembly/vtkWasmSceneManager.h b/Web/WebAssembly/vtkWasmSceneManager.h
new file mode 100644 (file)
index 0000000..9dd35c1
--- /dev/null
@@ -0,0 +1,114 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+/**
+ * @class   vtkWasmSceneManager
+ * @brief   vtkWasmSceneManager provides additional functionality that relates to a vtkRenderWindow
+ *          and user interaction.
+ *
+ * `vtkWasmSceneManager` is a javascript wrapper of `vtkSceneManager` for managing VTK
+ * objects, specifically designed for webassembly (wasm). It extends
+ * functionality of `vtkObjectManager` for managing objects such as `vtkRenderWindow`,
+ * `vtkRenderWindowInteractor` and enables event-observers in webassembly
+ * visualization applications.
+ *
+ * @sa vtkObjectManager
+ */
+#ifndef vtkWasmSceneManager_h
+#define vtkWasmSceneManager_h
+
+#include "vtkObjectManager.h"
+
+#include "vtkSerializationManagerModule.h" // for export macro
+
+VTK_ABI_NAMESPACE_BEGIN
+
+class VTKSERIALIZATIONMANAGER_EXPORT vtkWasmSceneManager : public vtkObjectManager
+{
+public:
+  static vtkWasmSceneManager* New();
+  vtkTypeMacro(vtkWasmSceneManager, vtkObjectManager);
+  void PrintSelf(ostream& os, vtkIndent indent) override;
+
+  bool Initialize() override;
+
+  /**
+   * Set the size of the `vtkRenderWindow` object at `identifier` to
+   * the supplied dimensions.
+   *
+   * Returns `true` if the object at `identifier` is a `vtkRenderWindow`
+   * with a `vtkRenderWindowInteractor` attached to it,
+   * `false` otherwise.
+   */
+  bool SetSize(vtkTypeUInt32 identifier, int width, int height);
+
+  /**
+   * Render the `vtkRenderWindow` object at `identifier`.
+   *
+   * Returns `true` if the object at `identifier` is a `vtkRenderWindow`
+   * `false` otherwise.
+   */
+  bool Render(vtkTypeUInt32 identifier);
+
+  /**
+   * Reset the active camera of the `vtkRenderer` object at `identifier`.
+   *
+   * Returns `true` if the object at `identifier` is a `vtkRenderer`
+   * `false` otherwise.
+   */
+  bool ResetCamera(vtkTypeUInt32 identifier);
+
+  /**
+   * Start event loop of the `vtkRenderWindowInteractor` object at `identifier`.
+   *
+   * Returns `true` if the object at `identifier` is a `vtkRenderWindowInteractor`
+   * `false` otherwise.
+   */
+  bool StartEventLoop(vtkTypeUInt32 identifier);
+
+  /**
+   * Stop event loop of the `vtkRenderWindowInteractor` object at `identifier`.
+   *
+   * Returns `true` if the object at `identifier` is a `vtkRenderWindowInteractor`
+   * `false` otherwise.
+   */
+  bool StopEventLoop(vtkTypeUInt32 identifier);
+
+  typedef void (*ObserverCallbackF)(vtkTypeUInt32, const char*);
+
+  /**
+   * Observes `eventName` event emitted by an object registered at `identifier`
+   * and invokes `callback` with the `identifier` and `eventName` for every such emission.
+   *
+   * Returns the tag of an observer for `eventName`. You can use the tag in `RemoveObserver`
+   * to stop observing `eventName` event from the object at `identifier`
+   */
+  unsigned long AddObserver(
+    vtkTypeUInt32 identifier, std::string eventName, ObserverCallbackF callback);
+
+  /**
+   * Stop observing the object at `identifier`.
+   * Returns `true` if an object exists at `identifier`,
+   * `false` otherwise.
+   */
+  bool RemoveObserver(vtkTypeUInt32 identifier, unsigned long tag);
+
+  /**
+   * Bind a `vtkRenderWindow` object at `renderWindowIdentifier` to a canvas element with the
+   * specified `canvasSelector`. This allows the `vtkRenderWindow` to render its content onto the
+   * specified HTML canvas element in a web application.
+   *
+   * @param renderWindowIdentifier The identifier of the `vtkRenderWindow` object to bind.
+   * @param canvasSelector The ID of the HTML canvas element to bind the `vtkRenderWindow` to.
+   */
+  bool BindRenderWindow(vtkTypeUInt32 renderWindowIdentifier, const char* canvasSelector);
+
+protected:
+  vtkWasmSceneManager();
+  ~vtkWasmSceneManager() override;
+
+private:
+  vtkWasmSceneManager(const vtkWasmSceneManager&) = delete;
+  void operator=(const vtkWasmSceneManager&) = delete;
+};
+VTK_ABI_NAMESPACE_END
+#endif
diff --git a/Web/WebAssembly/vtkWasmSceneManagerEmBinding.cxx b/Web/WebAssembly/vtkWasmSceneManagerEmBinding.cxx
new file mode 100644 (file)
index 0000000..68fc1bf
--- /dev/null
@@ -0,0 +1,463 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+#include <emscripten.h>
+#include <emscripten/bind.h>
+
+#include "vtkDataArrayRange.h"
+#include "vtkTypeUInt8Array.h"
+#include "vtkVersion.h"
+#include "vtkWasmSceneManager.h"
+
+// clang-format off
+#include "vtk_nlohmannjson.h"            // for json
+#include VTK_NLOHMANN_JSON(json_fwd.hpp) // for json
+// clang-format on
+
+#include <map>
+#include <set>
+
+namespace
+{
+
+#define CHECK_INIT                                                                                 \
+  do                                                                                               \
+  {                                                                                                \
+    if (Manager == nullptr)                                                                        \
+    {                                                                                              \
+      std::cerr << "Manager is null. Did you call forget to call initialize()?\n";                 \
+    }                                                                                              \
+  } while (0)
+
+vtkWasmSceneManager* Manager = nullptr;
+
+std::map<std::string, std::set<std::string>> SkippedClassProperties;
+
+using namespace emscripten;
+
+thread_local const val Uint8Array = val::global("Uint8Array");
+thread_local const val Uint32Array = val::global("Uint32Array");
+thread_local const val JSON = val::global("JSON");
+
+//-------------------------------------------------------------------------------
+bool initialize()
+{
+  Manager = vtkWasmSceneManager::New();
+  return Manager->Initialize();
+}
+
+//-------------------------------------------------------------------------------
+void finalize()
+{
+  CHECK_INIT;
+  Manager->UnRegister(nullptr);
+}
+
+//-------------------------------------------------------------------------------
+bool registerState(const std::string& state)
+{
+  CHECK_INIT;
+  auto stateJson = nlohmann::json::parse(state, nullptr, false);
+  if (stateJson.is_discarded())
+  {
+    vtkErrorWithObjectMacro(Manager, << "Failed to parse state!");
+    return false;
+  }
+  if (auto classNameIter = stateJson.find("ClassName"); classNameIter != stateJson.end())
+  {
+    if (*classNameIter == "vtkOSOpenGLRenderWindow")
+    {
+      *classNameIter = "vtkWebAssemblyOpenGLRenderWindow";
+    }
+    if (auto propertiesIter = SkippedClassProperties.find(*classNameIter);
+        propertiesIter != SkippedClassProperties.end())
+    {
+      for (const auto& propertyName : propertiesIter->second)
+      {
+        stateJson.erase(propertyName);
+      }
+    }
+  }
+  return Manager->RegisterState(stateJson);
+}
+
+//-------------------------------------------------------------------------------
+bool registerState(val stateJavaScriptJSON)
+{
+  CHECK_INIT;
+  const auto stringified = JSON.call<val>("stringify", stateJavaScriptJSON);
+  return ::registerState(stringified.as<std::string>());
+}
+
+//-------------------------------------------------------------------------------
+bool unRegisterState(vtkTypeUInt32 identifier)
+{
+  CHECK_INIT;
+  return Manager->UnRegisterState(identifier);
+}
+
+//-------------------------------------------------------------------------------
+val getState(vtkTypeUInt32 identifier)
+{
+  CHECK_INIT;
+  return JSON.call<val>("parse", Manager->GetState(identifier));
+}
+
+//-------------------------------------------------------------------------------
+void skipProperty(const std::string& className, const std::string& propertyName)
+{
+  SkippedClassProperties[className].insert(propertyName);
+}
+
+//-------------------------------------------------------------------------------
+void unSkipProperty(const std::string& className, const std::string& propertyName)
+{
+  SkippedClassProperties[className].erase(propertyName);
+}
+
+//-------------------------------------------------------------------------------
+bool unRegisterObject(vtkTypeUInt32 identifier)
+{
+  CHECK_INIT;
+  return Manager->UnRegisterObject(identifier);
+}
+
+//-------------------------------------------------------------------------------
+bool registerBlob(const std::string& hash, val jsArray)
+{
+  CHECK_INIT;
+  if (jsArray.instanceof (val::global("Uint8Array")))
+  {
+    const vtkIdType l = jsArray["length"].as<vtkIdType>();
+    auto blob = vtk::TakeSmartPointer(vtkTypeUInt8Array::New());
+    blob->SetNumberOfValues(l);
+    auto blobRange = vtk::DataArrayValueRange(blob);
+    val memoryView{ typed_memory_view(static_cast<std::size_t>(l), blobRange.data()) };
+    memoryView.call<void>("set", jsArray);
+    return Manager->RegisterBlob(hash, blob);
+  }
+  else
+  {
+    std::cerr << "Invalid type! Expects instanceof blob == Uint8Array" << std::endl;
+    return false;
+  }
+}
+
+//-------------------------------------------------------------------------------
+bool unRegisterBlob(const std::string& hash)
+{
+  CHECK_INIT;
+  return Manager->UnRegisterBlob(hash);
+}
+
+//-------------------------------------------------------------------------------
+val getBlob(const std::string& hash)
+{
+  CHECK_INIT;
+  const auto blob = Manager->GetBlob(hash);
+  val jsBlob = Uint8Array.new_(typed_memory_view(blob->GetNumberOfValues(), blob->GetPointer(0)));
+  return jsBlob;
+}
+
+//-------------------------------------------------------------------------------
+void pruneUnusedBlobs()
+{
+  CHECK_INIT;
+  Manager->PruneUnusedBlobs();
+}
+
+//-------------------------------------------------------------------------------
+void clear()
+{
+  CHECK_INIT;
+  Manager->Clear();
+}
+
+//-------------------------------------------------------------------------------
+val invoke(vtkTypeUInt32 identifier, const std::string& methodName, val args)
+{
+  CHECK_INIT;
+  const auto stringified = JSON.call<val>("stringify", args);
+  return JSON.call<val>(
+    "parse", Manager->Invoke(identifier, methodName, stringified.as<std::string>()));
+}
+
+//-------------------------------------------------------------------------------
+val getAllDependencies(vtkTypeUInt32 identifier)
+{
+  CHECK_INIT;
+  const auto ids = Manager->GetAllDependencies(identifier);
+  val jsIds = Uint32Array.new_(typed_memory_view(ids.size(), ids.data()));
+  return jsIds;
+}
+//-------------------------------------------------------------------------------
+std::size_t getTotalBlobMemoryUsage()
+{
+  CHECK_INIT;
+  return ::Manager->GetTotalBlobMemoryUsage();
+}
+
+//-------------------------------------------------------------------------------
+std::size_t getTotalVTKDataObjectMemoryUsage()
+{
+  CHECK_INIT;
+  return ::Manager->GetTotalVTKDataObjectMemoryUsage();
+}
+
+//-------------------------------------------------------------------------------
+void updateObjectsFromStates()
+{
+  CHECK_INIT;
+  Manager->UpdateObjectsFromStates();
+}
+
+//-------------------------------------------------------------------------------
+void updateStatesFromObjects()
+{
+  CHECK_INIT;
+  Manager->UpdateStatesFromObjects();
+}
+
+//-------------------------------------------------------------------------------
+void updateObjectFromState(const std::string& state)
+{
+  CHECK_INIT;
+  auto stateJson = nlohmann::json::parse(state, nullptr, false);
+  if (stateJson.is_discarded())
+  {
+    vtkErrorWithObjectMacro(Manager, << "Failed to parse state!");
+  }
+  else if (auto idIter = stateJson.find("Id"); idIter != stateJson.end())
+  {
+    if (auto classNameIter = stateJson.find("ClassName"); classNameIter != stateJson.end())
+    {
+      if (*classNameIter == "vtkOSOpenGLRenderWindow")
+      {
+        *classNameIter = "vtkWebAssemblyOpenGLRenderWindow";
+      }
+    }
+    if (auto objectAtId = Manager->GetObjectAtId(*idIter))
+    {
+      const std::string className = objectAtId->GetClassName();
+      if (auto propertiesIter = SkippedClassProperties.find(className);
+          propertiesIter != SkippedClassProperties.end())
+      {
+        for (const auto& propertyName : propertiesIter->second)
+        {
+          stateJson.erase(propertyName);
+        }
+      }
+    }
+  }
+  Manager->UpdateObjectFromState(stateJson);
+}
+
+//-------------------------------------------------------------------------------
+void updateObjectFromState(val stateJavaScriptJSON)
+{
+  CHECK_INIT;
+  const auto stringified = JSON.call<val>("stringify", stateJavaScriptJSON);
+  updateObjectFromState(stringified.as<std::string>());
+}
+
+//-------------------------------------------------------------------------------
+void updateStateFromObject(vtkTypeUInt32 identifier)
+{
+  CHECK_INIT;
+  Manager->UpdateStateFromObject(identifier);
+}
+
+//-------------------------------------------------------------------------------
+bool setSize(vtkTypeUInt32 identifier, int width, int height)
+{
+  CHECK_INIT;
+  return Manager->SetSize(identifier, width, height);
+}
+
+//-------------------------------------------------------------------------------
+bool render(vtkTypeUInt32 identifier)
+{
+  CHECK_INIT;
+  return Manager->Render(identifier);
+}
+
+//-------------------------------------------------------------------------------
+bool resetCamera(vtkTypeUInt32 identifier)
+{
+  CHECK_INIT;
+  return Manager->ResetCamera(identifier);
+}
+
+//-------------------------------------------------------------------------------
+bool startEventLoop(vtkTypeUInt32 identifier)
+{
+  CHECK_INIT;
+  return Manager->StartEventLoop(identifier);
+}
+
+//-------------------------------------------------------------------------------
+bool stopEventLoop(vtkTypeUInt32 identifier)
+{
+  CHECK_INIT;
+  return Manager->StopEventLoop(identifier);
+}
+
+//-------------------------------------------------------------------------------
+unsigned long addObserver(vtkTypeUInt32 identifier, std::string eventName, val jsFunc)
+{
+  CHECK_INIT;
+  int fp = val::module_property("addFunction")(jsFunc, std::string("vii")).as<int>();
+  auto callback = reinterpret_cast<vtkWasmSceneManager::ObserverCallbackF>(fp);
+  return Manager->AddObserver(identifier, eventName, callback);
+}
+
+//-------------------------------------------------------------------------------
+bool removeObserver(vtkTypeUInt32 identifier, unsigned long tag)
+{
+  CHECK_INIT;
+  return Manager->RemoveObserver(identifier, tag);
+}
+
+//-------------------------------------------------------------------------------
+bool bindRenderWindow(vtkTypeUInt32 renderWindowIdentifier, const std::string& canvasSelector)
+{
+  CHECK_INIT;
+  return Manager->BindRenderWindow(renderWindowIdentifier, canvasSelector.c_str());
+}
+
+//-------------------------------------------------------------------------------
+void import(const std::string& stateFileName, const std::string& blobFileName)
+{
+  CHECK_INIT;
+  Manager->Import(stateFileName, blobFileName);
+}
+
+//-------------------------------------------------------------------------------
+void printSceneManagerInformation()
+{
+  CHECK_INIT;
+  Manager->Print(std::cout);
+}
+
+//-------------------------------------------------------------------------------
+void setDeserializerLogVerbosity(const std::string& verbosityStr)
+{
+  CHECK_INIT;
+  const auto verbosity = vtkLogger::ConvertToVerbosity(verbosityStr.c_str());
+  if (verbosity != vtkLogger::VERBOSITY_INVALID)
+  {
+    Manager->GetDeserializer()->SetDeserializerLogVerbosity(verbosity);
+  }
+}
+
+//-------------------------------------------------------------------------------
+void setInvokerLogVerbosity(const std::string& verbosityStr)
+{
+  CHECK_INIT;
+  const auto verbosity = vtkLogger::ConvertToVerbosity(verbosityStr.c_str());
+  if (verbosity != vtkLogger::VERBOSITY_INVALID)
+  {
+    Manager->GetInvoker()->SetInvokerLogVerbosity(verbosity);
+  }
+}
+
+//-------------------------------------------------------------------------------
+void setObjectManagerLogVerbosity(const std::string& verbosityStr)
+{
+  CHECK_INIT;
+  const auto verbosity = vtkLogger::ConvertToVerbosity(verbosityStr.c_str());
+  if (verbosity != vtkLogger::VERBOSITY_INVALID)
+  {
+    Manager->SetObjectManagerLogVerbosity(verbosity);
+  }
+}
+
+//-------------------------------------------------------------------------------
+void setSerializerLogVerbosity(const std::string& verbosityStr)
+{
+  CHECK_INIT;
+  const auto verbosity = vtkLogger::ConvertToVerbosity(verbosityStr.c_str());
+  if (verbosity != vtkLogger::VERBOSITY_INVALID)
+  {
+    Manager->GetSerializer()->SetSerializerLogVerbosity(verbosity);
+  }
+}
+
+//-------------------------------------------------------------------------------
+std::string getVTKVersion()
+{
+  return vtkVersion::GetVTKVersion();
+}
+
+//-------------------------------------------------------------------------------
+std::string getVTKVersionFull()
+{
+  return vtkVersion::GetVTKVersionFull();
+}
+
+} // namespace
+
+EMSCRIPTEN_BINDINGS(vtkWasmSceneManager)
+{
+  function("initialize", ::initialize);
+  function("finalize", ::finalize);
+
+  function("registerState", select_overload<bool(const std::string&)>(::registerState));
+  function("registerStateJSON", select_overload<bool(val)>(::registerState));
+  function("unRegisterState", ::unRegisterState);
+  function("getState", ::getState);
+  function("skipProperty", ::skipProperty);
+  function("unSkipProperty", ::unSkipProperty);
+
+  function("unRegisterObject", ::unRegisterObject);
+
+  function("registerBlob", ::registerBlob);
+  function("unRegisterBlob", ::unRegisterBlob);
+  function("getBlob", ::getBlob);
+  function("pruneUnusedBlobs", ::pruneUnusedBlobs);
+
+  function("clear", ::clear);
+  function("invoke", ::invoke);
+
+  function("getAllDependencies", ::getAllDependencies);
+
+  function("getTotalBlobMemoryUsage", ::getTotalBlobMemoryUsage);
+  function("getTotalVTKDataObjectMemoryUsage", ::getTotalVTKDataObjectMemoryUsage);
+
+  function("updateObjectsFromStates", ::updateObjectsFromStates);
+  function("updateStatesFromObjects", ::updateStatesFromObjects);
+
+  function(
+    "updateObjectFromState", select_overload<void(const std::string&)>(::updateObjectFromState));
+  function("updateObjectFromStateJSON", select_overload<void(val)>(::updateObjectFromState));
+  function("updateStateFromObject", ::updateStateFromObject);
+
+  function("setSize", ::setSize);
+  function("render", ::render);
+  function("resetCamera", ::resetCamera);
+
+  function("startEventLoop", ::startEventLoop);
+  function("stopEventLoop", ::stopEventLoop);
+
+  function("addObserver", ::addObserver);
+  function("removeObserver", ::removeObserver);
+
+  function("bindRenderWindow", ::bindRenderWindow);
+
+  function("import", ::import);
+
+  // debugging
+  function("printSceneManagerInformation", ::printSceneManagerInformation);
+  // accepts JS strings like "INFO", "WARNING", "TRACE", "ERROR"
+  function("setDeserializerLogVerbosity", ::setDeserializerLogVerbosity);
+  function("setInvokerLogVerbosity", ::setInvokerLogVerbosity);
+  function("setObjectManagerLogVerbosity", ::setObjectManagerLogVerbosity);
+  function("setSerializerLogVerbosity", ::setSerializerLogVerbosity);
+
+  function("getVTKVersion", ::getVTKVersion);
+  function("getVTKVersionFull", ::getVTKVersionFull);
+}
+
+int main()
+{
+  return 0;
+}
diff --git a/Web/WebGLExporter/CMakeLists.txt b/Web/WebGLExporter/CMakeLists.txt
new file mode 100644 (file)
index 0000000..0edb3c2
--- /dev/null
@@ -0,0 +1,42 @@
+# The exporter will behave as any other ParaView exporter (VRML, X3D, POV...)
+# but will generate several types of files. The main one is the scene graph
+# description define as a JSON object with all the corresponding binary+base64
+# pieces that come along with it. But also with it come a single standalone HTML
+# file that can directly be used to see the data in a browser without any plugin.
+#
+# This code base should be cleaned up to follow VTK standard and even be
+# integrated into VTK itself. But for now it is provided as is.
+
+set(classes
+  vtkPVWebGLExporter
+  vtkWebGLDataSet
+  vtkWebGLExporter
+  vtkWebGLObject
+  vtkWebGLPolyData
+  vtkWebGLWidget)
+
+set(javascript_files
+  webglRenderer.js
+  glMatrix.js)
+
+set(sources)
+set(private_headers)
+
+foreach (javascript_file IN LISTS javascript_files)
+  vtk_encode_string(
+    INPUT         "${javascript_file}"
+    EXPORT_HEADER "vtkWebGLExporterModule.h"
+    EXPORT_SYMBOL "VTKWEBGLEXPORTER_NO_EXPORT"
+    HEADER_OUTPUT header
+    SOURCE_OUTPUT source)
+  list(APPEND sources
+    ${source})
+  list(APPEND private_headers
+    ${header})
+endforeach ()
+
+vtk_module_add_module(VTK::WebGLExporter
+  CLASSES ${classes}
+  SOURCES ${sources}
+  PRIVATE_HEADERS ${private_headers})
+vtk_add_test_mangling(VTK::WebGLExporter)
diff --git a/Web/WebGLExporter/glMatrix.js b/Web/WebGLExporter/glMatrix.js
new file mode 100644 (file)
index 0000000..4e4a830
--- /dev/null
@@ -0,0 +1,32 @@
+// glMatrix v0.9.5
+glMatrixArrayType=typeof Float32Array!="undefined"?Float32Array:typeof WebGLFloatArray!="undefined"?WebGLFloatArray:Array;var vec3={};vec3.create=function(a){var b=new glMatrixArrayType(3);if(a){b[0]=a[0];b[1]=a[1];b[2]=a[2]}return b};vec3.set=function(a,b){b[0]=a[0];b[1]=a[1];b[2]=a[2];return b};vec3.add=function(a,b,c){if(!c||a==c){a[0]+=b[0];a[1]+=b[1];a[2]+=b[2];return a}c[0]=a[0]+b[0];c[1]=a[1]+b[1];c[2]=a[2]+b[2];return c};
+vec3.subtract=function(a,b,c){if(!c||a==c){a[0]-=b[0];a[1]-=b[1];a[2]-=b[2];return a}c[0]=a[0]-b[0];c[1]=a[1]-b[1];c[2]=a[2]-b[2];return c};vec3.negate=function(a,b){b||(b=a);b[0]=-a[0];b[1]=-a[1];b[2]=-a[2];return b};vec3.scale=function(a,b,c){if(!c||a==c){a[0]*=b;a[1]*=b;a[2]*=b;return a}c[0]=a[0]*b;c[1]=a[1]*b;c[2]=a[2]*b;return c};
+vec3.normalize=function(a,b){b||(b=a);var c=a[0],d=a[1],e=a[2],g=Math.sqrt(c*c+d*d+e*e);if(g){if(g==1){b[0]=c;b[1]=d;b[2]=e;return b}}else{b[0]=0;b[1]=0;b[2]=0;return b}g=1/g;b[0]=c*g;b[1]=d*g;b[2]=e*g;return b};vec3.cross=function(a,b,c){c||(c=a);var d=a[0],e=a[1];a=a[2];var g=b[0],f=b[1];b=b[2];c[0]=e*b-a*f;c[1]=a*g-d*b;c[2]=d*f-e*g;return c};vec3.length=function(a){var b=a[0],c=a[1];a=a[2];return Math.sqrt(b*b+c*c+a*a)};vec3.dot=function(a,b){return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]};
+vec3.direction=function(a,b,c){c||(c=a);var d=a[0]-b[0],e=a[1]-b[1];a=a[2]-b[2];b=Math.sqrt(d*d+e*e+a*a);if(!b){c[0]=0;c[1]=0;c[2]=0;return c}b=1/b;c[0]=d*b;c[1]=e*b;c[2]=a*b;return c};vec3.lerp=function(a,b,c,d){d||(d=a);d[0]=a[0]+c*(b[0]-a[0]);d[1]=a[1]+c*(b[1]-a[1]);d[2]=a[2]+c*(b[2]-a[2]);return d};vec3.str=function(a){return"["+a[0]+", "+a[1]+", "+a[2]+"]"};var mat3={};
+mat3.create=function(a){var b=new glMatrixArrayType(9);if(a){b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=a[3];b[4]=a[4];b[5]=a[5];b[6]=a[6];b[7]=a[7];b[8]=a[8];b[9]=a[9]}return b};mat3.set=function(a,b){b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=a[3];b[4]=a[4];b[5]=a[5];b[6]=a[6];b[7]=a[7];b[8]=a[8];return b};mat3.identity=function(a){a[0]=1;a[1]=0;a[2]=0;a[3]=0;a[4]=1;a[5]=0;a[6]=0;a[7]=0;a[8]=1;return a};
+mat3.transpose=function(a,b){if(!b||a==b){var c=a[1],d=a[2],e=a[5];a[1]=a[3];a[2]=a[6];a[3]=c;a[5]=a[7];a[6]=d;a[7]=e;return a}b[0]=a[0];b[1]=a[3];b[2]=a[6];b[3]=a[1];b[4]=a[4];b[5]=a[7];b[6]=a[2];b[7]=a[5];b[8]=a[8];return b};mat3.toMat4=function(a,b){b||(b=mat4.create());b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=0;b[4]=a[3];b[5]=a[4];b[6]=a[5];b[7]=0;b[8]=a[6];b[9]=a[7];b[10]=a[8];b[11]=0;b[12]=0;b[13]=0;b[14]=0;b[15]=1;return b};
+mat3.str=function(a){return"["+a[0]+", "+a[1]+", "+a[2]+", "+a[3]+", "+a[4]+", "+a[5]+", "+a[6]+", "+a[7]+", "+a[8]+"]"};var mat4={};mat4.create=function(a){var b=new glMatrixArrayType(16);if(a){b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=a[3];b[4]=a[4];b[5]=a[5];b[6]=a[6];b[7]=a[7];b[8]=a[8];b[9]=a[9];b[10]=a[10];b[11]=a[11];b[12]=a[12];b[13]=a[13];b[14]=a[14];b[15]=a[15]}return b};
+mat4.set=function(a,b){b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=a[3];b[4]=a[4];b[5]=a[5];b[6]=a[6];b[7]=a[7];b[8]=a[8];b[9]=a[9];b[10]=a[10];b[11]=a[11];b[12]=a[12];b[13]=a[13];b[14]=a[14];b[15]=a[15];return b};mat4.identity=function(a){a[0]=1;a[1]=0;a[2]=0;a[3]=0;a[4]=0;a[5]=1;a[6]=0;a[7]=0;a[8]=0;a[9]=0;a[10]=1;a[11]=0;a[12]=0;a[13]=0;a[14]=0;a[15]=1;return a};
+mat4.transpose=function(a,b){if(!b||a==b){var c=a[1],d=a[2],e=a[3],g=a[6],f=a[7],h=a[11];a[1]=a[4];a[2]=a[8];a[3]=a[12];a[4]=c;a[6]=a[9];a[7]=a[13];a[8]=d;a[9]=g;a[11]=a[14];a[12]=e;a[13]=f;a[14]=h;return a}b[0]=a[0];b[1]=a[4];b[2]=a[8];b[3]=a[12];b[4]=a[1];b[5]=a[5];b[6]=a[9];b[7]=a[13];b[8]=a[2];b[9]=a[6];b[10]=a[10];b[11]=a[14];b[12]=a[3];b[13]=a[7];b[14]=a[11];b[15]=a[15];return b};
+mat4.determinant=function(a){var b=a[0],c=a[1],d=a[2],e=a[3],g=a[4],f=a[5],h=a[6],i=a[7],j=a[8],k=a[9],l=a[10],o=a[11],m=a[12],n=a[13],p=a[14];a=a[15];return m*k*h*e-j*n*h*e-m*f*l*e+g*n*l*e+j*f*p*e-g*k*p*e-m*k*d*i+j*n*d*i+m*c*l*i-b*n*l*i-j*c*p*i+b*k*p*i+m*f*d*o-g*n*d*o-m*c*h*o+b*n*h*o+g*c*p*o-b*f*p*o-j*f*d*a+g*k*d*a+j*c*h*a-b*k*h*a-g*c*l*a+b*f*l*a};
+mat4.inverse=function(a,b){b||(b=a);var c=a[0],d=a[1],e=a[2],g=a[3],f=a[4],h=a[5],i=a[6],j=a[7],k=a[8],l=a[9],o=a[10],m=a[11],n=a[12],p=a[13],r=a[14],s=a[15],A=c*h-d*f,B=c*i-e*f,t=c*j-g*f,u=d*i-e*h,v=d*j-g*h,w=e*j-g*i,x=k*p-l*n,y=k*r-o*n,z=k*s-m*n,C=l*r-o*p,D=l*s-m*p,E=o*s-m*r,q=1/(A*E-B*D+t*C+u*z-v*y+w*x);b[0]=(h*E-i*D+j*C)*q;b[1]=(-d*E+e*D-g*C)*q;b[2]=(p*w-r*v+s*u)*q;b[3]=(-l*w+o*v-m*u)*q;b[4]=(-f*E+i*z-j*y)*q;b[5]=(c*E-e*z+g*y)*q;b[6]=(-n*w+r*t-s*B)*q;b[7]=(k*w-o*t+m*B)*q;b[8]=(f*D-h*z+j*x)*q;
+b[9]=(-c*D+d*z-g*x)*q;b[10]=(n*v-p*t+s*A)*q;b[11]=(-k*v+l*t-m*A)*q;b[12]=(-f*C+h*y-i*x)*q;b[13]=(c*C-d*y+e*x)*q;b[14]=(-n*u+p*B-r*A)*q;b[15]=(k*u-l*B+o*A)*q;return b};mat4.toRotationMat=function(a,b){b||(b=mat4.create());b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=a[3];b[4]=a[4];b[5]=a[5];b[6]=a[6];b[7]=a[7];b[8]=a[8];b[9]=a[9];b[10]=a[10];b[11]=a[11];b[12]=0;b[13]=0;b[14]=0;b[15]=1;return b};
+mat4.toMat3=function(a,b){b||(b=mat3.create());b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=a[4];b[4]=a[5];b[5]=a[6];b[6]=a[8];b[7]=a[9];b[8]=a[10];return b};mat4.toInverseMat3=function(a,b){var c=a[0],d=a[1],e=a[2],g=a[4],f=a[5],h=a[6],i=a[8],j=a[9],k=a[10],l=k*f-h*j,o=-k*g+h*i,m=j*g-f*i,n=c*l+d*o+e*m;if(!n)return null;n=1/n;b||(b=mat3.create());b[0]=l*n;b[1]=(-k*d+e*j)*n;b[2]=(h*d-e*f)*n;b[3]=o*n;b[4]=(k*c-e*i)*n;b[5]=(-h*c+e*g)*n;b[6]=m*n;b[7]=(-j*c+d*i)*n;b[8]=(f*c-d*g)*n;return b};
+mat4.multiply=function(a,b,c){c||(c=a);var d=a[0],e=a[1],g=a[2],f=a[3],h=a[4],i=a[5],j=a[6],k=a[7],l=a[8],o=a[9],m=a[10],n=a[11],p=a[12],r=a[13],s=a[14];a=a[15];var A=b[0],B=b[1],t=b[2],u=b[3],v=b[4],w=b[5],x=b[6],y=b[7],z=b[8],C=b[9],D=b[10],E=b[11],q=b[12],F=b[13],G=b[14];b=b[15];c[0]=A*d+B*h+t*l+u*p;c[1]=A*e+B*i+t*o+u*r;c[2]=A*g+B*j+t*m+u*s;c[3]=A*f+B*k+t*n+u*a;c[4]=v*d+w*h+x*l+y*p;c[5]=v*e+w*i+x*o+y*r;c[6]=v*g+w*j+x*m+y*s;c[7]=v*f+w*k+x*n+y*a;c[8]=z*d+C*h+D*l+E*p;c[9]=z*e+C*i+D*o+E*r;c[10]=z*
+g+C*j+D*m+E*s;c[11]=z*f+C*k+D*n+E*a;c[12]=q*d+F*h+G*l+b*p;c[13]=q*e+F*i+G*o+b*r;c[14]=q*g+F*j+G*m+b*s;c[15]=q*f+F*k+G*n+b*a;return c};mat4.multiplyVec3=function(a,b,c){c||(c=b);var d=b[0],e=b[1];b=b[2];c[0]=a[0]*d+a[4]*e+a[8]*b+a[12];c[1]=a[1]*d+a[5]*e+a[9]*b+a[13];c[2]=a[2]*d+a[6]*e+a[10]*b+a[14];return c};
+mat4.multiplyVec4=function(a,b,c){c||(c=b);var d=b[0],e=b[1],g=b[2];b=b[3];c[0]=a[0]*d+a[4]*e+a[8]*g+a[12]*b;c[1]=a[1]*d+a[5]*e+a[9]*g+a[13]*b;c[2]=a[2]*d+a[6]*e+a[10]*g+a[14]*b;c[3]=a[3]*d+a[7]*e+a[11]*g+a[15]*b;return c};
+mat4.translate=function(a,b,c){var d=b[0],e=b[1];b=b[2];if(!c||a==c){a[12]=a[0]*d+a[4]*e+a[8]*b+a[12];a[13]=a[1]*d+a[5]*e+a[9]*b+a[13];a[14]=a[2]*d+a[6]*e+a[10]*b+a[14];a[15]=a[3]*d+a[7]*e+a[11]*b+a[15];return a}var g=a[0],f=a[1],h=a[2],i=a[3],j=a[4],k=a[5],l=a[6],o=a[7],m=a[8],n=a[9],p=a[10],r=a[11];c[0]=g;c[1]=f;c[2]=h;c[3]=i;c[4]=j;c[5]=k;c[6]=l;c[7]=o;c[8]=m;c[9]=n;c[10]=p;c[11]=r;c[12]=g*d+j*e+m*b+a[12];c[13]=f*d+k*e+n*b+a[13];c[14]=h*d+l*e+p*b+a[14];c[15]=i*d+o*e+r*b+a[15];return c};
+mat4.scale=function(a,b,c){var d=b[0],e=b[1];b=b[2];if(!c||a==c){a[0]*=d;a[1]*=d;a[2]*=d;a[3]*=d;a[4]*=e;a[5]*=e;a[6]*=e;a[7]*=e;a[8]*=b;a[9]*=b;a[10]*=b;a[11]*=b;return a}c[0]=a[0]*d;c[1]=a[1]*d;c[2]=a[2]*d;c[3]=a[3]*d;c[4]=a[4]*e;c[5]=a[5]*e;c[6]=a[6]*e;c[7]=a[7]*e;c[8]=a[8]*b;c[9]=a[9]*b;c[10]=a[10]*b;c[11]=a[11]*b;c[12]=a[12];c[13]=a[13];c[14]=a[14];c[15]=a[15];return c};
+mat4.rotate=function(a,b,c,d){var e=c[0],g=c[1];c=c[2];var f=Math.sqrt(e*e+g*g+c*c);if(!f)return null;if(f!=1){f=1/f;e*=f;g*=f;c*=f}var h=Math.sin(b),i=Math.cos(b),j=1-i;b=a[0];f=a[1];var k=a[2],l=a[3],o=a[4],m=a[5],n=a[6],p=a[7],r=a[8],s=a[9],A=a[10],B=a[11],t=e*e*j+i,u=g*e*j+c*h,v=c*e*j-g*h,w=e*g*j-c*h,x=g*g*j+i,y=c*g*j+e*h,z=e*c*j+g*h;e=g*c*j-e*h;g=c*c*j+i;if(d){if(a!=d){d[12]=a[12];d[13]=a[13];d[14]=a[14];d[15]=a[15]}}else d=a;d[0]=b*t+o*u+r*v;d[1]=f*t+m*u+s*v;d[2]=k*t+n*u+A*v;d[3]=l*t+p*u+B*
+v;d[4]=b*w+o*x+r*y;d[5]=f*w+m*x+s*y;d[6]=k*w+n*x+A*y;d[7]=l*w+p*x+B*y;d[8]=b*z+o*e+r*g;d[9]=f*z+m*e+s*g;d[10]=k*z+n*e+A*g;d[11]=l*z+p*e+B*g;return d};mat4.rotateX=function(a,b,c){var d=Math.sin(b);b=Math.cos(b);var e=a[4],g=a[5],f=a[6],h=a[7],i=a[8],j=a[9],k=a[10],l=a[11];if(c){if(a!=c){c[0]=a[0];c[1]=a[1];c[2]=a[2];c[3]=a[3];c[12]=a[12];c[13]=a[13];c[14]=a[14];c[15]=a[15]}}else c=a;c[4]=e*b+i*d;c[5]=g*b+j*d;c[6]=f*b+k*d;c[7]=h*b+l*d;c[8]=e*-d+i*b;c[9]=g*-d+j*b;c[10]=f*-d+k*b;c[11]=h*-d+l*b;return c};
+mat4.rotateY=function(a,b,c){var d=Math.sin(b);b=Math.cos(b);var e=a[0],g=a[1],f=a[2],h=a[3],i=a[8],j=a[9],k=a[10],l=a[11];if(c){if(a!=c){c[4]=a[4];c[5]=a[5];c[6]=a[6];c[7]=a[7];c[12]=a[12];c[13]=a[13];c[14]=a[14];c[15]=a[15]}}else c=a;c[0]=e*b+i*-d;c[1]=g*b+j*-d;c[2]=f*b+k*-d;c[3]=h*b+l*-d;c[8]=e*d+i*b;c[9]=g*d+j*b;c[10]=f*d+k*b;c[11]=h*d+l*b;return c};
+mat4.rotateZ=function(a,b,c){var d=Math.sin(b);b=Math.cos(b);var e=a[0],g=a[1],f=a[2],h=a[3],i=a[4],j=a[5],k=a[6],l=a[7];if(c){if(a!=c){c[8]=a[8];c[9]=a[9];c[10]=a[10];c[11]=a[11];c[12]=a[12];c[13]=a[13];c[14]=a[14];c[15]=a[15]}}else c=a;c[0]=e*b+i*d;c[1]=g*b+j*d;c[2]=f*b+k*d;c[3]=h*b+l*d;c[4]=e*-d+i*b;c[5]=g*-d+j*b;c[6]=f*-d+k*b;c[7]=h*-d+l*b;return c};
+mat4.frustum=function(a,b,c,d,e,g,f){f||(f=mat4.create());var h=b-a,i=d-c,j=g-e;f[0]=e*2/h;f[1]=0;f[2]=0;f[3]=0;f[4]=0;f[5]=e*2/i;f[6]=0;f[7]=0;f[8]=(b+a)/h;f[9]=(d+c)/i;f[10]=-(g+e)/j;f[11]=-1;f[12]=0;f[13]=0;f[14]=-(g*e*2)/j;f[15]=0;return f};mat4.perspective=function(a,b,c,d,e){a=c*Math.tan(a*Math.PI/360);b=a*b;return mat4.frustum(-b,b,-a,a,c,d,e)};
+mat4.ortho=function(a,b,c,d,e,g,f){f||(f=mat4.create());var h=b-a,i=d-c,j=g-e;f[0]=2/h;f[1]=0;f[2]=0;f[3]=0;f[4]=0;f[5]=2/i;f[6]=0;f[7]=0;f[8]=0;f[9]=0;f[10]=-2/j;f[11]=0;f[12]=-(a+b)/h;f[13]=-(d+c)/i;f[14]=-(g+e)/j;f[15]=1;return f};
+mat4.lookAt=function(a,b,c,d){d||(d=mat4.create());var e=a[0],g=a[1];a=a[2];var f=c[0],h=c[1],i=c[2];c=b[1];var j=b[2];if(e==b[0]&&g==c&&a==j)return mat4.identity(d);var k,l,o,m;c=e-b[0];j=g-b[1];b=a-b[2];m=1/Math.sqrt(c*c+j*j+b*b);c*=m;j*=m;b*=m;k=h*b-i*j;i=i*c-f*b;f=f*j-h*c;if(m=Math.sqrt(k*k+i*i+f*f)){m=1/m;k*=m;i*=m;f*=m}else f=i=k=0;h=j*f-b*i;l=b*k-c*f;o=c*i-j*k;if(m=Math.sqrt(h*h+l*l+o*o)){m=1/m;h*=m;l*=m;o*=m}else o=l=h=0;d[0]=k;d[1]=h;d[2]=c;d[3]=0;d[4]=i;d[5]=l;d[6]=j;d[7]=0;d[8]=f;d[9]=
+o;d[10]=b;d[11]=0;d[12]=-(k*e+i*g+f*a);d[13]=-(h*e+l*g+o*a);d[14]=-(c*e+j*g+b*a);d[15]=1;return d};mat4.str=function(a){return"["+a[0]+", "+a[1]+", "+a[2]+", "+a[3]+", "+a[4]+", "+a[5]+", "+a[6]+", "+a[7]+", "+a[8]+", "+a[9]+", "+a[10]+", "+a[11]+", "+a[12]+", "+a[13]+", "+a[14]+", "+a[15]+"]"};quat4={};quat4.create=function(a){var b=new glMatrixArrayType(4);if(a){b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=a[3]}return b};quat4.set=function(a,b){b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=a[3];return b};
+quat4.calculateW=function(a,b){var c=a[0],d=a[1],e=a[2];if(!b||a==b){a[3]=-Math.sqrt(Math.abs(1-c*c-d*d-e*e));return a}b[0]=c;b[1]=d;b[2]=e;b[3]=-Math.sqrt(Math.abs(1-c*c-d*d-e*e));return b};quat4.inverse=function(a,b){if(!b||a==b){a[0]*=1;a[1]*=1;a[2]*=1;return a}b[0]=-a[0];b[1]=-a[1];b[2]=-a[2];b[3]=a[3];return b};quat4.length=function(a){var b=a[0],c=a[1],d=a[2];a=a[3];return Math.sqrt(b*b+c*c+d*d+a*a)};
+quat4.normalize=function(a,b){b||(b=a);var c=a[0],d=a[1],e=a[2],g=a[3],f=Math.sqrt(c*c+d*d+e*e+g*g);if(f==0){b[0]=0;b[1]=0;b[2]=0;b[3]=0;return b}f=1/f;b[0]=c*f;b[1]=d*f;b[2]=e*f;b[3]=g*f;return b};quat4.multiply=function(a,b,c){c||(c=a);var d=a[0],e=a[1],g=a[2];a=a[3];var f=b[0],h=b[1],i=b[2];b=b[3];c[0]=d*b+a*f+e*i-g*h;c[1]=e*b+a*h+g*f-d*i;c[2]=g*b+a*i+d*h-e*f;c[3]=a*b-d*f-e*h-g*i;return c};
+quat4.multiplyVec3=function(a,b,c){c||(c=b);var d=b[0],e=b[1],g=b[2];b=a[0];var f=a[1],h=a[2];a=a[3];var i=a*d+f*g-h*e,j=a*e+h*d-b*g,k=a*g+b*e-f*d;d=-b*d-f*e-h*g;c[0]=i*a+d*-b+j*-h-k*-f;c[1]=j*a+d*-f+k*-b-i*-h;c[2]=k*a+d*-h+i*-f-j*-b;return c};quat4.toMat3=function(a,b){b||(b=mat3.create());var c=a[0],d=a[1],e=a[2],g=a[3],f=c+c,h=d+d,i=e+e,j=c*f,k=c*h;c=c*i;var l=d*h;d=d*i;e=e*i;f=g*f;h=g*h;g=g*i;b[0]=1-(l+e);b[1]=k-g;b[2]=c+h;b[3]=k+g;b[4]=1-(j+e);b[5]=d-f;b[6]=c-h;b[7]=d+f;b[8]=1-(j+l);return b};
+quat4.toMat4=function(a,b){b||(b=mat4.create());var c=a[0],d=a[1],e=a[2],g=a[3],f=c+c,h=d+d,i=e+e,j=c*f,k=c*h;c=c*i;var l=d*h;d=d*i;e=e*i;f=g*f;h=g*h;g=g*i;b[0]=1-(l+e);b[1]=k-g;b[2]=c+h;b[3]=0;b[4]=k+g;b[5]=1-(j+e);b[6]=d-f;b[7]=0;b[8]=c-h;b[9]=d+f;b[10]=1-(j+l);b[11]=0;b[12]=0;b[13]=0;b[14]=0;b[15]=1;return b};quat4.slerp=function(a,b,c,d){d||(d=a);var e=c;if(a[0]*b[0]+a[1]*b[1]+a[2]*b[2]+a[3]*b[3]<0)e=-1*c;d[0]=1-c*a[0]+e*b[0];d[1]=1-c*a[1]+e*b[1];d[2]=1-c*a[2]+e*b[2];d[3]=1-c*a[3]+e*b[3];return d};
+quat4.str=function(a){return"["+a[0]+", "+a[1]+", "+a[2]+", "+a[3]+"]"};
diff --git a/Web/WebGLExporter/vtk.module b/Web/WebGLExporter/vtk.module
new file mode 100644 (file)
index 0000000..c58d06a
--- /dev/null
@@ -0,0 +1,23 @@
+NAME
+  VTK::WebGLExporter
+LIBRARY_NAME
+  vtkWebGLExporter
+GROUPS
+  Web
+SPDX_LICENSE_IDENTIFIER
+  BSD-3-Clause
+SPDX_COPYRIGHT_TEXT
+  Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+DEPENDS
+  VTK::CommonCore
+  VTK::IOExport
+PRIVATE_DEPENDS
+  VTK::CommonDataModel
+  VTK::CommonMath
+  VTK::FiltersCore
+  VTK::FiltersGeometry
+  VTK::IOCore
+  VTK::InteractionWidgets
+  VTK::RenderingAnnotation
+  VTK::RenderingCore
+  VTK::vtksys
diff --git a/Web/WebGLExporter/vtkPVWebGLExporter.cxx b/Web/WebGLExporter/vtkPVWebGLExporter.cxx
new file mode 100644 (file)
index 0000000..3b8b9d8
--- /dev/null
@@ -0,0 +1,125 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+#include "vtkPVWebGLExporter.h"
+
+#include "vtkBase64Utilities.h"
+#include "vtkCamera.h"
+#include "vtkExporter.h"
+#include "vtkNew.h"
+#include "vtkObjectFactory.h"
+#include "vtkRenderWindow.h"
+#include "vtkRenderer.h"
+#include "vtkRendererCollection.h"
+#include "vtkWebGLExporter.h"
+#include "vtkWebGLObject.h"
+
+#include <fstream>
+#include <sstream>
+#include <string>
+#include <vtksys/FStream.hxx>
+#include <vtksys/SystemTools.hxx>
+
+VTK_ABI_NAMESPACE_BEGIN
+vtkStandardNewMacro(vtkPVWebGLExporter);
+//------------------------------------------------------------------------------
+vtkPVWebGLExporter::vtkPVWebGLExporter()
+{
+  this->FileName = nullptr;
+}
+
+//------------------------------------------------------------------------------
+vtkPVWebGLExporter::~vtkPVWebGLExporter()
+{
+  this->SetFileName(nullptr);
+}
+
+//------------------------------------------------------------------------------
+void vtkPVWebGLExporter::WriteData()
+{
+  // make sure the user specified a FileName or FilePointer
+  if (this->FileName == nullptr)
+  {
+    vtkErrorMacro(<< "Please specify FileName to use");
+    return;
+  }
+
+  vtkNew<vtkWebGLExporter> exporter;
+  exporter->SetMaxAllowedSize(65000);
+
+  // We use the camera focal point to be the center of rotation
+  double centerOfRotation[3];
+  vtkRenderer* ren = this->RenderWindow->GetRenderers()->GetFirstRenderer();
+  vtkCamera* cam = ren->GetActiveCamera();
+  cam->GetFocalPoint(centerOfRotation);
+  exporter->SetCenterOfRotation(static_cast<float>(centerOfRotation[0]),
+    static_cast<float>(centerOfRotation[1]), static_cast<float>(centerOfRotation[2]));
+
+  exporter->parseScene(this->RenderWindow->GetRenderers(), "1", VTK_PARSEALL);
+
+  // Write meta-data file
+  std::string baseFileName = this->FileName;
+  baseFileName.erase(baseFileName.size() - 6, 6);
+  std::string metadatFile = this->FileName;
+  FILE* fp = vtksys::SystemTools::Fopen(metadatFile, "w");
+  if (!fp)
+  {
+    vtkErrorMacro(<< "unable to open JSON MetaData file " << metadatFile);
+    return;
+  }
+  fputs(exporter->GenerateMetadata(), fp);
+  fclose(fp);
+
+  // Write binary objects
+  vtkNew<vtkBase64Utilities> base64;
+  int nbObjects = exporter->GetNumberOfObjects();
+  for (int idx = 0; idx < nbObjects; ++idx)
+  {
+    vtkWebGLObject* obj = exporter->GetWebGLObject(idx);
+    if (obj->isVisible())
+    {
+      int nbParts = obj->GetNumberOfParts();
+      for (int part = 0; part < nbParts; ++part)
+      {
+        // Manage binary content
+        std::stringstream filePath;
+        filePath << baseFileName << "_" << obj->GetMD5() << "_" << part;
+        vtksys::ofstream binaryFile;
+        binaryFile.open(filePath.str().c_str(), std::ios_base::out | std::ios_base::binary);
+        binaryFile.write((const char*)obj->GetBinaryData(part), obj->GetBinarySize(part));
+        binaryFile.close();
+
+        // Manage Base64
+        std::stringstream filePathBase64;
+        filePathBase64 << baseFileName << "_" << obj->GetMD5() << "_" << part << ".base64";
+        vtksys::ofstream base64File;
+        unsigned char* output = new unsigned char[obj->GetBinarySize(part) * 2];
+        int size =
+          base64->Encode(obj->GetBinaryData(part), obj->GetBinarySize(part), output, false);
+        base64File.open(filePathBase64.str().c_str(), std::ios_base::out);
+        base64File.write((const char*)output, size);
+        base64File.close();
+        delete[] output;
+      }
+    }
+  }
+
+  // Write HTML file
+  std::string htmlFile = baseFileName;
+  htmlFile += ".html";
+  exporter->exportStaticScene(this->RenderWindow->GetRenderers(), 300, 300, htmlFile);
+}
+//------------------------------------------------------------------------------
+void vtkPVWebGLExporter::PrintSelf(ostream& os, vtkIndent indent)
+{
+  this->Superclass::PrintSelf(os, indent);
+
+  if (this->FileName)
+  {
+    os << indent << "FileName: " << this->FileName << "\n";
+  }
+  else
+  {
+    os << indent << "FileName: (null)\n";
+  }
+}
+VTK_ABI_NAMESPACE_END
diff --git a/Web/WebGLExporter/vtkPVWebGLExporter.h b/Web/WebGLExporter/vtkPVWebGLExporter.h
new file mode 100644 (file)
index 0000000..4e74ede
--- /dev/null
@@ -0,0 +1,36 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+#ifndef vtkPVWebGLExporter_h
+#define vtkPVWebGLExporter_h
+
+#include "vtkExporter.h"
+#include "vtkWebGLExporterModule.h" // needed for export macro
+
+VTK_ABI_NAMESPACE_BEGIN
+class VTKWEBGLEXPORTER_EXPORT vtkPVWebGLExporter : public vtkExporter
+{
+public:
+  static vtkPVWebGLExporter* New();
+  vtkTypeMacro(vtkPVWebGLExporter, vtkExporter);
+  void PrintSelf(ostream& os, vtkIndent indent) override;
+
+  // Description:
+  // Specify the name of the VRML file to write.
+  vtkSetFilePathMacro(FileName);
+  vtkGetFilePathMacro(FileName);
+
+protected:
+  vtkPVWebGLExporter();
+  ~vtkPVWebGLExporter() override;
+
+  void WriteData() override;
+
+  char* FileName;
+
+private:
+  vtkPVWebGLExporter(const vtkPVWebGLExporter&) = delete;
+  void operator=(const vtkPVWebGLExporter&) = delete;
+};
+
+VTK_ABI_NAMESPACE_END
+#endif
diff --git a/Web/WebGLExporter/vtkWebGLDataSet.cxx b/Web/WebGLExporter/vtkWebGLDataSet.cxx
new file mode 100644 (file)
index 0000000..ab89bc2
--- /dev/null
@@ -0,0 +1,235 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include "vtkWebGLDataSet.h"
+
+#include "vtkObjectFactory.h"
+#include "vtkWebGLExporter.h"
+
+VTK_ABI_NAMESPACE_BEGIN
+vtkStandardNewMacro(vtkWebGLDataSet);
+
+std::string vtkWebGLDataSet::GetMD5()
+{
+  return this->MD5;
+}
+
+vtkWebGLDataSet::vtkWebGLDataSet()
+{
+  this->NumberOfVertices = 0;
+  this->NumberOfPoints = 0;
+  this->NumberOfIndexes = 0;
+  this->vertices = nullptr;
+  this->normals = nullptr;
+  this->indexes = nullptr;
+  this->points = nullptr;
+  this->tcoords = nullptr;
+  this->colors = nullptr;
+  this->binary = nullptr;
+  this->binarySize = 0;
+  this->hasChanged = false;
+}
+
+vtkWebGLDataSet::~vtkWebGLDataSet()
+{
+  delete[] this->vertices;
+  delete[] this->normals;
+  delete[] this->indexes;
+  delete[] this->points;
+  delete[] this->tcoords;
+  delete[] this->colors;
+  delete[] this->binary;
+}
+
+void vtkWebGLDataSet::SetVertices(float* v, int size)
+{
+  delete[] this->vertices;
+  this->vertices = v;
+  this->NumberOfVertices = size;
+  this->webGLType = wTRIANGLES;
+  this->hasChanged = true;
+}
+
+void vtkWebGLDataSet::SetIndexes(short* i, int size)
+{
+  delete[] this->indexes;
+  this->indexes = i;
+  this->NumberOfIndexes = size;
+  this->hasChanged = true;
+}
+
+void vtkWebGLDataSet::SetNormals(float* n)
+{
+  delete[] this->normals;
+  this->normals = n;
+  this->hasChanged = true;
+}
+
+void vtkWebGLDataSet::SetColors(unsigned char* c)
+{
+  delete[] this->colors;
+  this->colors = c;
+  this->hasChanged = true;
+}
+
+void vtkWebGLDataSet::SetPoints(float* p, int size)
+{
+  delete[] this->points;
+  this->points = p;
+  this->NumberOfPoints = size;
+  this->webGLType = wLINES;
+  this->hasChanged = true;
+}
+
+void vtkWebGLDataSet::SetTCoords(float* t)
+{
+  delete[] this->tcoords;
+  this->tcoords = t;
+  this->hasChanged = true;
+}
+
+unsigned char* vtkWebGLDataSet::GetBinaryData()
+{
+  this->hasChanged = false;
+  return this->binary;
+}
+
+int vtkWebGLDataSet::GetBinarySize()
+{
+  return this->binarySize;
+}
+
+void vtkWebGLDataSet::SetMatrix(float* m)
+{
+  this->Matrix = m;
+  this->hasChanged = true;
+}
+
+void vtkWebGLDataSet::GenerateBinaryData()
+{
+  if (this->NumberOfIndexes == 0 && this->webGLType != wPOINTS)
+  {
+    return;
+  }
+  int size = 0, pos = 0, total = 0;
+  delete[] this->binary;
+  this->binarySize = 0;
+
+  if (this->webGLType == wLINES)
+  {
+    pos = sizeof(pos);
+    size = this->NumberOfPoints * sizeof(this->points[0]);
+
+    // Calculate the size used by each data
+    total = sizeof(pos) + 1 + sizeof(this->NumberOfPoints) +
+      size * 3 // Size, Type, NumberOfPoints, Points
+      + sizeof(this->colors[0]) * this->NumberOfPoints * 4 +
+      sizeof(this->NumberOfIndexes) // Color, NumberOfIndex
+      + this->NumberOfIndexes * sizeof(this->indexes[0]) +
+      sizeof(this->Matrix[0]) * 16; // Index, Matrix
+    this->binary = new unsigned char[total];
+    memset(this->binary, 0, total);
+
+    this->binary[pos++] = 'L';
+    memcpy(&this->binary[pos], &this->NumberOfPoints, sizeof(this->NumberOfPoints));
+    pos += sizeof(this->NumberOfPoints); // Points
+    memcpy(&this->binary[pos], this->points, size * 3);
+    pos += size * 3;
+    memcpy(&this->binary[pos], this->colors, sizeof(this->colors[0]) * this->NumberOfPoints * 4);
+    pos += sizeof(this->colors[0]) * this->NumberOfPoints * 4;
+    memcpy(&this->binary[pos], &this->NumberOfIndexes, sizeof(this->NumberOfIndexes));
+    pos += sizeof(this->NumberOfIndexes);
+    memcpy(&this->binary[pos], this->indexes, this->NumberOfIndexes * sizeof(this->indexes[0]));
+    pos += this->NumberOfIndexes * sizeof(this->indexes[0]);
+    memcpy(&this->binary[pos], this->Matrix, sizeof(this->Matrix[0]) * 16);
+    pos += sizeof(this->Matrix[0]) * 16; // Matrix
+
+    memcpy(&this->binary[0], &pos, sizeof(pos));
+    this->binarySize = total;
+  }
+  else if (this->webGLType == wTRIANGLES)
+  {
+    pos = sizeof(pos);
+    size = sizeof(this->vertices[0]) * this->NumberOfVertices;
+
+    // Calculate the size used by each data
+    total = sizeof(pos) + 1 + sizeof(this->NumberOfVertices) +
+      size * (3 + 3) // Size, Type, VertCount, Vert, Normal
+      + sizeof(this->colors[0]) * this->NumberOfVertices * 4 +
+      sizeof(this->NumberOfIndexes) // Color, IndicCount
+      + this->NumberOfIndexes * sizeof(this->indexes[0]) +
+      sizeof(this->Matrix[0]) * 16; // Index, Matrix
+    if (this->tcoords)
+      total += size * 2; // TCoord
+    this->binary = new unsigned char[total];
+    memset(this->binary, 0, total);
+
+    this->binary[pos++] = 'M';
+    memcpy(&this->binary[pos], &this->NumberOfVertices, sizeof(this->NumberOfVertices));
+    pos += sizeof(this->NumberOfVertices); // VertCount
+    memcpy(&this->binary[pos], this->vertices, size * 3);
+    pos += size * 3; // Vertices
+    memcpy(&this->binary[pos], this->normals, size * 3);
+    pos += size * 3; // Normals
+    memcpy(&this->binary[pos], this->colors, sizeof(this->colors[0]) * this->NumberOfVertices * 4);
+    pos += sizeof(this->colors[0]) * this->NumberOfVertices * 4; // Colors
+    memcpy(&this->binary[pos], &this->NumberOfIndexes, sizeof(this->NumberOfIndexes));
+    pos += sizeof(this->NumberOfIndexes); // IndCount
+    memcpy(&this->binary[pos], this->indexes, this->NumberOfIndexes * sizeof(this->indexes[0]));
+    pos += this->NumberOfIndexes * sizeof(this->indexes[0]);
+    memcpy(&this->binary[pos], this->Matrix, sizeof(this->Matrix[0]) * 16);
+    pos += sizeof(this->Matrix[0]) * 16; // Matrix
+    if (this->tcoords)                   // TCoord
+    {
+      memcpy(&this->binary[pos], this->tcoords, size * 2);
+      pos += size * 2;
+    }
+
+    memcpy(&this->binary[0], &pos, sizeof(pos));
+    this->binarySize = total;
+  }
+  else if (this->webGLType == wPOINTS)
+  {
+    pos = sizeof(pos);
+    size = this->NumberOfPoints * sizeof(this->points[0]);
+
+    // Calculate the size used by each data
+    total = sizeof(pos) + 1 + sizeof(this->NumberOfPoints) +
+      size * 3 // Size, Type, NumberOfPoints, Points
+      + sizeof(this->colors[0]) * this->NumberOfPoints * 4 +
+      sizeof(this->Matrix[0]) * 16; // Color, Matrix
+    this->binary = new unsigned char[total];
+    memset(this->binary, 0, total);
+
+    this->binary[pos++] = 'P';
+    memcpy(&this->binary[pos], &this->NumberOfPoints, sizeof(this->NumberOfPoints));
+    pos += sizeof(this->NumberOfPoints); // Points
+    memcpy(&this->binary[pos], this->points, size * 3);
+    pos += size * 3;
+    memcpy(&this->binary[pos], this->colors, sizeof(this->colors[0]) * this->NumberOfPoints * 4);
+    pos += sizeof(this->colors[0]) * this->NumberOfPoints * 4;
+    memcpy(&this->binary[pos], this->Matrix, sizeof(this->Matrix[0]) * 16);
+    pos += sizeof(this->Matrix[0]) * 16; // Matrix
+
+    memcpy(&this->binary[0], &pos, sizeof(pos));
+    this->binarySize = total;
+  }
+  vtkWebGLExporter::ComputeMD5((const unsigned char*)this->binary, this->binarySize, this->MD5);
+  this->hasChanged = true;
+}
+
+void vtkWebGLDataSet::SetType(WebGLObjectTypes t)
+{
+  this->webGLType = t;
+}
+
+bool vtkWebGLDataSet::HasChanged()
+{
+  return this->hasChanged;
+}
+
+void vtkWebGLDataSet::PrintSelf(ostream& os, vtkIndent indent)
+{
+  this->Superclass::PrintSelf(os, indent);
+}
+VTK_ABI_NAMESPACE_END
diff --git a/Web/WebGLExporter/vtkWebGLDataSet.h b/Web/WebGLExporter/vtkWebGLDataSet.h
new file mode 100644 (file)
index 0000000..e86bc6a
--- /dev/null
@@ -0,0 +1,68 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+/**
+ * @class   vtkWebGLDataSet
+ * @brief   vtkWebGLDataSet represent vertices, lines, polygons, and triangles.
+ */
+
+#ifndef vtkWebGLDataSet_h
+#define vtkWebGLDataSet_h
+
+#include "vtkObject.h"
+#include "vtkWebGLExporterModule.h" // needed for export macro
+
+#include "vtkWebGLObject.h" // Needed for the enum
+#include <string>           // needed for md5
+
+VTK_ABI_NAMESPACE_BEGIN
+class VTKWEBGLEXPORTER_EXPORT vtkWebGLDataSet : public vtkObject
+{
+public:
+  static vtkWebGLDataSet* New();
+  vtkTypeMacro(vtkWebGLDataSet, vtkObject);
+  void PrintSelf(ostream& os, vtkIndent indent) override;
+
+  void SetVertices(float* v, int size);
+  void SetIndexes(short* i, int size);
+  void SetNormals(float* n);
+  void SetColors(unsigned char* c);
+  void SetPoints(float* p, int size);
+  void SetTCoords(float* t);
+  void SetMatrix(float* m);
+  void SetType(WebGLObjectTypes t);
+
+  unsigned char* GetBinaryData();
+  int GetBinarySize();
+  void GenerateBinaryData();
+  bool HasChanged();
+
+  std::string GetMD5();
+
+protected:
+  vtkWebGLDataSet();
+  ~vtkWebGLDataSet() override;
+
+  int NumberOfVertices;
+  int NumberOfPoints;
+  int NumberOfIndexes;
+  WebGLObjectTypes webGLType;
+
+  float* Matrix;
+  float* vertices;
+  float* normals;
+  short* indexes;
+  float* points;
+  float* tcoords;
+  unsigned char* colors;
+  unsigned char* binary; // Data in binary
+  int binarySize;        // Size of the data in binary
+  bool hasChanged;
+  std::string MD5;
+
+private:
+  vtkWebGLDataSet(const vtkWebGLDataSet&) = delete;
+  void operator=(const vtkWebGLDataSet&) = delete;
+};
+
+VTK_ABI_NAMESPACE_END
+#endif
diff --git a/Web/WebGLExporter/vtkWebGLExporter.cxx b/Web/WebGLExporter/vtkWebGLExporter.cxx
new file mode 100644 (file)
index 0000000..f130c88
--- /dev/null
@@ -0,0 +1,788 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include "vtkWebGLExporter.h"
+
+#include "vtkAbstractMapper.h"
+#include "vtkActor2D.h"
+#include "vtkActorCollection.h"
+#include "vtkBase64Utilities.h"
+#include "vtkCamera.h"
+#include "vtkCellArray.h"
+#include "vtkCellData.h"
+#include "vtkCompositeDataGeometryFilter.h"
+#include "vtkCompositeDataSet.h"
+#include "vtkDataSet.h"
+#include "vtkDataSetAttributes.h"
+#include "vtkDiscretizableColorTransferFunction.h"
+#include "vtkFollower.h"
+#include "vtkGenericCell.h"
+#include "vtkMapper.h"
+#include "vtkMapper2D.h"
+#include "vtkMatrix4x4.h"
+#include "vtkObjectFactory.h"
+#include "vtkPointData.h"
+#include "vtkPolyDataMapper2D.h"
+#include "vtkPolyDataNormals.h"
+#include "vtkProperty.h"
+#include "vtkProperty2D.h"
+#include "vtkRenderWindow.h"
+#include "vtkRenderer.h"
+#include "vtkRendererCollection.h"
+#include "vtkScalarBarActor.h"
+#include "vtkScalarBarRepresentation.h"
+#include "vtkSmartPointer.h"
+#include "vtkTriangleFilter.h"
+#include "vtkViewport.h"
+#include "vtkWidgetRepresentation.h"
+
+#include "vtkWebGLObject.h"
+#include "vtkWebGLPolyData.h"
+#include "vtkWebGLWidget.h"
+
+#include <algorithm>
+#include <iostream>
+#include <map>
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include "glMatrix.h"
+#include "webglRenderer.h"
+
+#include "vtksys/FStream.hxx"
+#include "vtksys/MD5.h"
+#include "vtksys/SystemTools.hxx"
+
+//*****************************************************************************
+class vtkWebGLExporter::vtkInternal
+{
+public:
+  std::string LastMetaData;
+  std::map<vtkProp*, vtkMTimeType> ActorTimestamp;
+  std::map<vtkProp*, vtkMTimeType> OldActorTimestamp;
+  std::vector<vtkWebGLObject*> Objects;
+  std::vector<vtkWebGLObject*> tempObj;
+};
+//*****************************************************************************
+
+vtkStandardNewMacro(vtkWebGLExporter);
+
+vtkWebGLExporter::vtkWebGLExporter()
+{
+  this->meshObjMaxSize = 65532 / 3;
+  this->lineObjMaxSize = 65534 / 2;
+  this->Internal = new vtkInternal();
+  this->TriangleFilter = nullptr;
+  this->GradientBackground = false;
+  this->SetCenterOfRotation(0.0, 0.0, 0.0);
+  this->renderersMetaData = "";
+  this->SceneSize[0] = 0;
+  this->SceneSize[1] = 0;
+  this->SceneSize[2] = 0;
+  this->hasWidget = false;
+}
+
+vtkWebGLExporter::~vtkWebGLExporter()
+{
+  while (!this->Internal->Objects.empty())
+  {
+    vtkWebGLObject* obj = this->Internal->Objects.back();
+    obj->Delete();
+    this->Internal->Objects.pop_back();
+  }
+  delete this->Internal;
+  if (this->TriangleFilter)
+  {
+    this->TriangleFilter->Delete();
+  }
+}
+
+void vtkWebGLExporter::SetMaxAllowedSize(int mesh, int lines)
+{
+  this->meshObjMaxSize = mesh;
+  this->lineObjMaxSize = lines;
+  if (this->meshObjMaxSize * 3 > 65532)
+    this->meshObjMaxSize = 65532 / 3;
+  if (this->lineObjMaxSize * 2 > 65534)
+    this->lineObjMaxSize = 65534 / 2;
+  if (this->meshObjMaxSize < 10)
+    this->meshObjMaxSize = 10;
+  if (this->lineObjMaxSize < 10)
+    this->lineObjMaxSize = 10;
+  for (size_t i = 0; i < this->Internal->Objects.size(); i++)
+    this->Internal->Objects[i]->GenerateBinaryData();
+}
+
+void vtkWebGLExporter::SetMaxAllowedSize(int size)
+{
+  this->SetMaxAllowedSize(size, size);
+}
+
+void vtkWebGLExporter::SetCenterOfRotation(float a1, float a2, float a3)
+{
+  this->CenterOfRotation[0] = a1;
+  this->CenterOfRotation[1] = a2;
+  this->CenterOfRotation[2] = a3;
+}
+
+void vtkWebGLExporter::parseRenderer(
+  vtkRenderer* renderer, const char* vtkNotUsed(viewId), bool onlyWidget, void* vtkNotUsed(mapTime))
+{
+  vtkPropCollection* propCollection = renderer->GetViewProps();
+  for (int i = 0; i < propCollection->GetNumberOfItems(); i++)
+  {
+    vtkProp* prop = (vtkProp*)propCollection->GetItemAsObject(i);
+    vtkWidgetRepresentation* trt = vtkWidgetRepresentation::SafeDownCast(prop);
+    if (trt != nullptr)
+      this->hasWidget = true;
+    if ((!onlyWidget || trt != nullptr) && prop->GetVisibility())
+    {
+      vtkPropCollection* allactors = vtkPropCollection::New();
+      prop->GetActors(allactors);
+      for (int j = 0; j < allactors->GetNumberOfItems(); j++)
+      {
+        vtkActor* actor = vtkActor::SafeDownCast(allactors->GetItemAsObject(j));
+        vtkActor* key = actor;
+        vtkMTimeType previousValue = this->Internal->OldActorTimestamp[key];
+        this->parseActor(
+          actor, previousValue, (size_t)renderer, renderer->GetLayer(), trt != nullptr);
+      }
+      allactors->Delete();
+    }
+    if (!onlyWidget && prop->GetVisibility())
+    {
+      vtkPropCollection* all2dactors = vtkPropCollection::New();
+      prop->GetActors2D(all2dactors);
+      for (int k = 0; k < all2dactors->GetNumberOfItems(); k++)
+      {
+        vtkActor2D* actor = vtkActor2D::SafeDownCast(all2dactors->GetItemAsObject(k));
+        vtkActor2D* key = actor;
+        vtkMTimeType previousValue = this->Internal->OldActorTimestamp[key];
+        this->parseActor2D(
+          actor, previousValue, (size_t)renderer, renderer->GetLayer(), trt != nullptr);
+      }
+      all2dactors->Delete();
+    }
+  }
+}
+
+void vtkWebGLExporter::parseActor2D(
+  vtkActor2D* actor, vtkMTimeType actorTime, size_t renderId, int layer, bool isWidget)
+{
+  vtkActor2D* key = actor;
+  vtkScalarBarActor* scalarbar = vtkScalarBarActor::SafeDownCast(actor);
+
+  vtkMTimeType dataMTime =
+    actor->GetMTime() + actor->GetRedrawMTime() + actor->GetProperty()->GetMTime();
+  dataMTime += (vtkMTimeType)actor->GetMapper();
+  if (scalarbar)
+    dataMTime += scalarbar->GetLookupTable()->GetMTime();
+  if (dataMTime != actorTime && actor->GetVisibility())
+  {
+    this->Internal->ActorTimestamp[key] = dataMTime;
+
+    if (actor->GetMapper())
+    {
+      if (vtkPolyDataMapper2D::SafeDownCast(actor->GetMapper()))
+      {
+      }
+    }
+    else
+    {
+      if (scalarbar)
+      {
+        vtkWebGLWidget* obj = vtkWebGLWidget::New();
+        obj->GetDataFromColorMap(actor);
+
+        std::stringstream ss;
+        ss << (size_t)actor;
+        obj->SetId(ss.str());
+        obj->SetRendererId(static_cast<int>(renderId));
+        this->Internal->Objects.push_back(obj);
+        obj->SetLayer(layer);
+        obj->SetVisibility(actor->GetVisibility() != 0);
+        obj->SetIsWidget(isWidget);
+        obj->SetInteractAtServer(false);
+        obj->GenerateBinaryData();
+      }
+    }
+  }
+  else
+  {
+    this->Internal->ActorTimestamp[key] = dataMTime;
+    std::stringstream ss;
+    ss << (vtkMTimeType)actor;
+    for (size_t i = 0; i < this->Internal->tempObj.size(); i++)
+    {
+      if (this->Internal->tempObj[i]->GetId() == ss.str())
+      {
+        vtkWebGLObject* obj = this->Internal->tempObj[i];
+        this->Internal->tempObj.erase(this->Internal->tempObj.begin() + i);
+        obj->SetVisibility(actor->GetVisibility() != 0);
+        this->Internal->Objects.push_back(obj);
+      }
+    }
+  }
+}
+
+void vtkWebGLExporter::parseActor(
+  vtkActor* actor, vtkMTimeType actorTime, size_t rendererId, int layer, bool isWidget)
+{
+  vtkMapper* mapper = actor->GetMapper();
+  if (mapper)
+  {
+    vtkMTimeType dataMTime;
+    vtkTriangleFilter* polydata = this->GetPolyData(mapper, dataMTime);
+    vtkActor* key = actor;
+    dataMTime = actor->GetMTime() + mapper->GetLookupTable()->GetMTime();
+    dataMTime += actor->GetProperty()->GetMTime() + mapper->GetMTime() + actor->GetRedrawMTime();
+    dataMTime +=
+      polydata->GetOutput()->GetNumberOfLines() + polydata->GetOutput()->GetNumberOfPolys();
+    dataMTime +=
+      actor->GetProperty()->GetRepresentation() + mapper->GetScalarMode() + actor->GetVisibility();
+    dataMTime += polydata->GetInput()->GetMTime();
+    if (vtkFollower::SafeDownCast(actor))
+      dataMTime += vtkFollower::SafeDownCast(actor)->GetCamera()->GetMTime();
+    if (dataMTime != actorTime && actor->GetVisibility())
+    {
+      double bb[6];
+      actor->GetBounds(bb);
+      double m1 = std::max(bb[1] - bb[0], bb[3] - bb[2]);
+      m1 = std::max(m1, bb[5] - bb[4]);
+      double m2 = std::max(this->SceneSize[0], this->SceneSize[1]);
+      m2 = std::max(m2, this->SceneSize[2]);
+      if (m1 > m2)
+      {
+        this->SceneSize[0] = bb[1] - bb[0];
+        this->SceneSize[1] = bb[3] - bb[2];
+        this->SceneSize[2] = bb[5] - bb[4];
+      }
+
+      this->Internal->ActorTimestamp[key] = dataMTime;
+      vtkWebGLObject* obj = nullptr;
+      std::stringstream ss;
+      ss << (size_t)actor;
+      for (size_t i = 0; i < this->Internal->tempObj.size(); i++)
+      {
+        if (this->Internal->tempObj[i]->GetId() == ss.str())
+        {
+          obj = this->Internal->tempObj[i];
+          this->Internal->tempObj.erase(this->Internal->tempObj.begin() + i);
+        }
+      }
+      if (obj == nullptr)
+        obj = vtkWebGLPolyData::New();
+
+      if (polydata->GetOutput()->GetNumberOfPolys() != 0)
+      {
+        if (actor->GetProperty()->GetRepresentation() == VTK_WIREFRAME)
+        {
+          ((vtkWebGLPolyData*)obj)
+            ->GetLinesFromPolygon(mapper, actor, this->lineObjMaxSize, nullptr);
+        }
+        else
+        {
+
+          if (actor->GetProperty()->GetEdgeVisibility())
+          {
+            vtkWebGLPolyData* newobj = vtkWebGLPolyData::New();
+            double ccc[3];
+            actor->GetProperty()->GetEdgeColor(&ccc[0]);
+            newobj->GetLinesFromPolygon(mapper, actor, this->lineObjMaxSize, ccc);
+            newobj->SetId(ss.str() + "1");
+            newobj->SetRendererId(static_cast<int>(rendererId));
+            this->Internal->Objects.push_back(newobj);
+            newobj->SetLayer(layer);
+            newobj->SetTransformationMatrix(actor->GetMatrix());
+            newobj->SetVisibility(actor->GetVisibility() != 0);
+            newobj->SetHasTransparency(actor->HasTranslucentPolygonalGeometry() != 0);
+            newobj->SetIsWidget(isWidget);
+            newobj->SetInteractAtServer(isWidget);
+            newobj->GenerateBinaryData();
+          }
+
+          switch (mapper->GetScalarMode())
+          {
+            case VTK_SCALAR_MODE_USE_POINT_FIELD_DATA:
+              ((vtkWebGLPolyData*)obj)
+                ->GetPolygonsFromPointData(polydata, actor, this->meshObjMaxSize);
+              break;
+            case VTK_SCALAR_MODE_USE_CELL_FIELD_DATA:
+              ((vtkWebGLPolyData*)obj)
+                ->GetPolygonsFromCellData(polydata, actor, this->meshObjMaxSize);
+              break;
+            default:
+              ((vtkWebGLPolyData*)obj)
+                ->GetPolygonsFromPointData(polydata, actor, this->meshObjMaxSize);
+              break;
+          }
+        }
+        obj->SetId(ss.str());
+        obj->SetRendererId(static_cast<int>(rendererId));
+        this->Internal->Objects.push_back(obj);
+        obj->SetLayer(layer);
+        obj->SetTransformationMatrix(actor->GetMatrix());
+        obj->SetVisibility(actor->GetVisibility() != 0);
+        obj->SetHasTransparency(actor->HasTranslucentPolygonalGeometry() != 0);
+        obj->SetIsWidget(isWidget);
+        obj->SetInteractAtServer(isWidget);
+        obj->GenerateBinaryData();
+      }
+      else if (polydata->GetOutput()->GetNumberOfLines() != 0)
+      {
+        ((vtkWebGLPolyData*)obj)->GetLines(polydata, actor, this->lineObjMaxSize);
+        obj->SetId(ss.str());
+        obj->SetRendererId(static_cast<int>(rendererId));
+        this->Internal->Objects.push_back(obj);
+        obj->SetLayer(layer);
+        obj->SetTransformationMatrix(actor->GetMatrix());
+        obj->SetVisibility(actor->GetVisibility() != 0);
+        obj->SetHasTransparency(actor->HasTranslucentPolygonalGeometry() != 0);
+        obj->SetIsWidget(isWidget);
+        obj->SetInteractAtServer(isWidget);
+        obj->GenerateBinaryData();
+      }
+      else if (polydata->GetOutput()->GetNumberOfPoints() != 0)
+      {
+        ((vtkWebGLPolyData*)obj)->GetPoints(polydata, actor, 65534); // Wendel
+        obj->SetId(ss.str());
+        obj->SetRendererId(static_cast<int>(rendererId));
+        this->Internal->Objects.push_back(obj);
+        obj->SetLayer(layer);
+        obj->SetTransformationMatrix(actor->GetMatrix());
+        obj->SetVisibility(actor->GetVisibility() != 0);
+        obj->SetHasTransparency(actor->HasTranslucentPolygonalGeometry() != 0);
+        obj->SetIsWidget(false);
+        obj->SetInteractAtServer(false);
+        obj->GenerateBinaryData();
+      }
+
+      if (polydata->GetOutput()->GetNumberOfPolys() != 0 &&
+        polydata->GetOutput()->GetNumberOfLines() != 0)
+      {
+        obj = vtkWebGLPolyData::New();
+        ((vtkWebGLPolyData*)obj)->GetLines(polydata, actor, this->lineObjMaxSize);
+        ss << "1";
+        obj->SetId(ss.str());
+        obj->SetRendererId(static_cast<int>(rendererId));
+        this->Internal->Objects.push_back(obj);
+        obj->SetLayer(layer);
+        obj->SetTransformationMatrix(actor->GetMatrix());
+        obj->SetVisibility(actor->GetVisibility() != 0);
+        obj->SetHasTransparency(actor->HasTranslucentPolygonalGeometry() != 0);
+        obj->SetIsWidget(isWidget);
+        obj->SetInteractAtServer(isWidget);
+        obj->GenerateBinaryData();
+      }
+
+      if (polydata->GetOutput()->GetNumberOfLines() == 0 &&
+        polydata->GetOutput()->GetNumberOfPolys() == 0 &&
+        polydata->GetOutput()->GetNumberOfPoints() == 0)
+      {
+        obj->Delete();
+      }
+    }
+    else
+    {
+      this->Internal->ActorTimestamp[key] = actorTime;
+      std::stringstream ss;
+      ss << (size_t)actor;
+      for (size_t i = 0; i < this->Internal->tempObj.size(); i++)
+      {
+        if (this->Internal->tempObj[i]->GetId() == ss.str())
+        {
+          vtkWebGLObject* obj = this->Internal->tempObj[i];
+          this->Internal->tempObj.erase(this->Internal->tempObj.begin() + i);
+          obj->SetVisibility(actor->GetVisibility() != 0);
+          this->Internal->Objects.push_back(obj);
+        }
+      }
+    }
+  }
+}
+
+void vtkWebGLExporter::parseScene(
+  vtkRendererCollection* renderers, const char* viewId, int parseType)
+{
+  if (!renderers)
+    return;
+
+  bool onlyWidget = parseType == VTK_ONLYWIDGET;
+  bool cameraOnly = onlyWidget && !this->hasWidget;
+
+  this->SceneId = viewId ? viewId : "";
+  if (cameraOnly)
+  {
+    this->generateRendererData(renderers, viewId);
+    return;
+  }
+
+  if (onlyWidget)
+  {
+    for (int i = static_cast<int>(this->Internal->Objects.size()) - 1; i >= 0; i--)
+    {
+      vtkWebGLObject* obj = this->Internal->Objects[i];
+      if (obj->InteractAtServer())
+      {
+        this->Internal->tempObj.push_back(obj);
+        this->Internal->Objects.erase(this->Internal->Objects.begin() + i);
+      }
+    }
+  }
+  else
+  {
+    while (!this->Internal->Objects.empty())
+    {
+      this->Internal->tempObj.push_back(this->Internal->Objects.back());
+      this->Internal->Objects.pop_back();
+    }
+  }
+
+  this->Internal->OldActorTimestamp = this->Internal->ActorTimestamp;
+  if (!onlyWidget)
+    this->Internal->ActorTimestamp.clear();
+  this->hasWidget = false;
+  for (int i = 0; i < renderers->GetNumberOfItems(); i++)
+  {
+    vtkRenderer* renderer = vtkRenderer::SafeDownCast(renderers->GetItemAsObject(i));
+    if (renderer->GetDraw())
+      this->parseRenderer(renderer, viewId, onlyWidget, nullptr);
+  }
+  while (!this->Internal->tempObj.empty())
+  {
+    vtkWebGLObject* obj = this->Internal->tempObj.back();
+    this->Internal->tempObj.pop_back();
+    obj->Delete();
+  }
+
+  this->generateRendererData(renderers, viewId);
+}
+
+bool sortLayer(vtkRenderer* i, vtkRenderer* j)
+{
+  return (i->GetLayer() < j->GetLayer());
+}
+
+void vtkWebGLExporter::generateRendererData(
+  vtkRendererCollection* renderers, const char* vtkNotUsed(viewId))
+{
+  std::stringstream ss;
+  ss << "\"Renderers\": [";
+
+  std::vector<vtkRenderer*> orderedList;
+  orderedList.reserve(renderers->GetNumberOfItems());
+  for (int i = 0; i < renderers->GetNumberOfItems(); i++)
+    orderedList.push_back(vtkRenderer::SafeDownCast(renderers->GetItemAsObject(i)));
+  std::sort(orderedList.begin(), orderedList.begin() + orderedList.size(), sortLayer);
+
+  int* fullSize = nullptr;
+  for (size_t i = 0; i < orderedList.size(); i++)
+  {
+    vtkRenderer* renderer = orderedList[i];
+
+    if (i == 0)
+    {
+      fullSize = renderer->GetSize();
+    }
+
+    double cam[10];
+    cam[0] = renderer->GetActiveCamera()->GetViewAngle();
+    renderer->GetActiveCamera()->GetFocalPoint(&cam[1]);
+    renderer->GetActiveCamera()->GetViewUp(&cam[4]);
+    renderer->GetActiveCamera()->GetPosition(&cam[7]);
+    int *s, *o;
+    s = renderer->GetSize();
+    o = renderer->GetOrigin();
+    ss << "{\"layer\":" << renderer->GetLayer() << ","; // Render Layer
+    if (renderer->GetLayer() == 0)                      // Render Background
+    {
+      double back[3];
+      renderer->GetBackground(back);
+      ss << "\"Background1\":[" << back[0] << "," << back[1] << "," << back[2] << "],";
+      if (renderer->GetGradientBackground())
+      {
+        renderer->GetBackground2(back);
+        ss << "\"Background2\":[" << back[0] << "," << back[1] << "," << back[2] << "],";
+      }
+    }
+    ss << "\"LookAt\":["; // Render Camera
+    for (int j = 0; j < 9; j++)
+      ss << cam[j] << ",";
+    ss << cam[9] << "], ";
+    ss << "\"size\": [" << (float)(s[0] / (float)fullSize[0]) << ","
+       << (float)(s[1] / (float)fullSize[1]) << "],"; // Render Size
+    ss << "\"origin\": [" << (float)(o[0] / (float)fullSize[0]) << ","
+       << (float)(o[1] / (float)fullSize[1]) << "]"; // Render Position
+    ss << "}";
+    if (static_cast<int>(i + 1) != renderers->GetNumberOfItems())
+      ss << ", ";
+  }
+  ss << "]";
+  this->renderersMetaData = ss.str();
+}
+
+vtkTriangleFilter* vtkWebGLExporter::GetPolyData(vtkMapper* mapper, vtkMTimeType& dataMTime)
+{
+  vtkDataSet* dataset = nullptr;
+  vtkSmartPointer<vtkDataSet> tempDS;
+  vtkDataObject* dObj = mapper->GetInputDataObject(0, 0);
+  vtkCompositeDataSet* cd = vtkCompositeDataSet::SafeDownCast(dObj);
+  if (cd)
+  {
+    dataMTime = cd->GetMTime();
+    vtkCompositeDataGeometryFilter* gf = vtkCompositeDataGeometryFilter::New();
+    gf->SetInputData(cd);
+    gf->Update();
+    tempDS = gf->GetOutput();
+    gf->Delete();
+    dataset = tempDS;
+  }
+  else
+  {
+    dataset = mapper->GetInput();
+    dataMTime = dataset->GetMTime();
+  }
+
+  // Converting to triangles. WebGL only support triangles.
+  if (this->TriangleFilter)
+    this->TriangleFilter->Delete();
+  this->TriangleFilter = vtkTriangleFilter::New();
+  this->TriangleFilter->SetInputData(dataset);
+  this->TriangleFilter->Update();
+  return this->TriangleFilter;
+}
+
+/*
+  Function: GenerateMetaData
+  Description:
+  - Generates the metadata of the scene in JSON format
+  Ex.:
+    { "id": ,"LookAt": ,"Background1": ,"Background2":
+    "Objects": [{"id": ,"md5": ,"parts": },  {"id": ,"md5": ,"parts": }] }
+*/
+VTK_ABI_NAMESPACE_BEGIN
+const char* vtkWebGLExporter::GenerateMetadata()
+{
+  double max = std::max(this->SceneSize[0], this->SceneSize[1]);
+  max = std::max(max, this->SceneSize[2]);
+  std::stringstream ss;
+
+  ss << "{\"id\":" << this->SceneId << ",";
+  ss << "\"MaxSize\":" << max << ",";
+  ss << "\"Center\":[";
+  for (int i = 0; i < 2; i++)
+    ss << this->CenterOfRotation[i] << ", ";
+  ss << this->CenterOfRotation[2] << "],";
+
+  ss << this->renderersMetaData << ",";
+
+  ss << " \"Objects\":[";
+  bool first = true;
+  for (size_t i = 0; i < this->Internal->Objects.size(); i++)
+  {
+    vtkWebGLObject* obj = this->Internal->Objects[i];
+    if (obj->isVisible())
+    {
+      if (first)
+        first = false;
+      else
+        ss << ", ";
+      ss << "{\"id\":" << obj->GetId() << ", \"md5\":\"" << obj->GetMD5() << "\""
+         << ", \"parts\":" << obj->GetNumberOfParts()
+         << ", \"interactAtServer\":" << obj->InteractAtServer()
+         << ", \"transparency\":" << obj->HasTransparency() << ", \"layer\":" << obj->GetLayer()
+         << ", \"wireframe\":" << obj->isWireframeMode() << "}";
+    }
+  }
+  ss << "]}";
+
+  this->Internal->LastMetaData = ss.str();
+  return this->Internal->LastMetaData.c_str();
+}
+
+const char* vtkWebGLExporter::GenerateExportMetadata()
+{
+  double max = std::max(this->SceneSize[0], this->SceneSize[1]);
+  max = std::max(max, this->SceneSize[2]);
+  std::stringstream ss;
+
+  ss << "{\"id\":" << this->SceneId << ",";
+  ss << "\"MaxSize\":" << max << ",";
+  ss << "\"Center\":[";
+  for (int i = 0; i < 2; i++)
+    ss << this->CenterOfRotation[i] << ", ";
+  ss << this->CenterOfRotation[2] << "],";
+
+  ss << this->renderersMetaData << ",";
+
+  ss << " \"Objects\":[";
+  bool first = true;
+  for (size_t i = 0; i < this->Internal->Objects.size(); i++)
+  {
+    vtkWebGLObject* obj = this->Internal->Objects[i];
+    if (obj->isVisible())
+    {
+      for (int j = 0; j < obj->GetNumberOfParts(); j++)
+      {
+        if (first)
+          first = false;
+        else
+          ss << ", ";
+        ss << "{\"id\":" << obj->GetId() << ", \"md5\":\"" << obj->GetMD5() << "\""
+           << ", \"parts\":" << 1 << ", \"interactAtServer\":" << obj->InteractAtServer()
+           << ", \"transparency\":" << obj->HasTransparency() << ", \"layer\":" << obj->GetLayer()
+           << ", \"wireframe\":" << obj->isWireframeMode() << "}";
+      }
+    }
+  }
+  ss << "]}";
+
+  this->Internal->LastMetaData = ss.str();
+  return this->Internal->LastMetaData.c_str();
+}
+
+vtkWebGLObject* vtkWebGLExporter::GetWebGLObject(int index)
+{
+  return this->Internal->Objects[index];
+}
+
+int vtkWebGLExporter::GetNumberOfObjects()
+{
+  return static_cast<int>(this->Internal->Objects.size());
+}
+
+void vtkWebGLExporter::PrintSelf(ostream& os, vtkIndent indent)
+{
+  this->Superclass::PrintSelf(os, indent);
+}
+
+const char* vtkWebGLExporter::GetId()
+{
+  return this->SceneId.c_str();
+}
+
+bool vtkWebGLExporter::hasChanged()
+{
+  for (size_t i = 0; i < this->Internal->Objects.size(); i++)
+    if (this->Internal->Objects[i]->HasChanged())
+      return true;
+  return false;
+}
+
+void vtkWebGLExporter::exportStaticScene(
+  vtkRendererCollection* renderers, int width, int height, std::string path)
+{
+  std::stringstream ss;
+  ss << width << "," << height;
+  std::string resultHTML =
+    "<html><head></head><body onload='loadStaticScene();' style='margin: 0px; padding: 0px; "
+    "position: absolute; overflow: hidden; top:0px; left:0px;'>";
+  resultHTML += "<div id='container' onclick='consumeEvent(event);' style='margin: 0px; padding: "
+                "0px; position: absolute; overflow: hidden; top:0px; left:0px;'></div></body>\n";
+  resultHTML += "<script type='text/javascript'> var rendererWebGL = null;";
+  resultHTML += "function reresize(event){ if (rendererWebGL != null) "
+                "rendererWebGL.setSize(window.innerWidth, window.innerHeight); }";
+  resultHTML += "function loadStaticScene(){ ";
+  resultHTML += "  var objs=[];";
+  resultHTML += "  for(i=0; i<object.length; i++){";
+  resultHTML += "  objs[i] = decode64(object[i]);";
+  resultHTML += "  }\n object = [];";
+  resultHTML += "  rendererWebGL = new WebGLRenderer('webglRenderer-1', '');";
+  resultHTML += "  rendererWebGL.init('', '');";
+  resultHTML += "  rendererWebGL.bindToElementId('container');";
+  resultHTML += "  //rendererWebGL.setSize(" + ss.str() + ");\n";
+  resultHTML += "  rendererWebGL.setSize(window.innerWidth, window.innerHeight);";
+  resultHTML += "  rendererWebGL.start(metadata, objs);";
+  resultHTML += "  window.onresize = reresize;";
+  resultHTML += "}\n";
+  resultHTML += "function consumeEvent(event) { if (event.preventDefault) { "
+                "event.preventDefault();} else { event.returnValue= false;} return false;}";
+
+  resultHTML += "function ntos(n){ n=n.toString(16); if (n.length == 1) n='0'+n; n='%'+n; return "
+                "unescape(n); }";
+  resultHTML += "var END_OF_INPUT = -1; var base64Chars = new Array(";
+  resultHTML += "'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','"
+                "U','V','W','X',";
+  resultHTML += "'Y','Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','"
+                "s','t','u','v',";
+  resultHTML += "'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/');";
+  resultHTML += "var base64Str; var base64Count;";
+  resultHTML += "var reverseBase64Chars = new Array();";
+  resultHTML +=
+    "for (var i=0; i < base64Chars.length; i++){ reverseBase64Chars[base64Chars[i]] = i; }";
+  resultHTML += "function readReverseBase64(){ if (!base64Str) return END_OF_INPUT;";
+  resultHTML += "while (true){ if (base64Count >= base64Str.length) return END_OF_INPUT;";
+  resultHTML += "var nextCharacter = base64Str.charAt(base64Count); base64Count++;";
+  resultHTML +=
+    "if (reverseBase64Chars[nextCharacter]){ return reverseBase64Chars[nextCharacter]; }";
+  resultHTML += "if (nextCharacter == 'A') return 0; } return END_OF_INPUT; }";
+  resultHTML += "function decode64(str){";
+  resultHTML += "base64Str = str; base64Count = 0; var result = ''; var inBuffer = new Array(4); "
+                "var done = false;";
+  resultHTML += "while (!done && (inBuffer[0] = readReverseBase64()) != END_OF_INPUT";
+  resultHTML += "&& (inBuffer[1] = readReverseBase64()) != END_OF_INPUT){";
+  resultHTML += "inBuffer[2] = readReverseBase64();";
+  resultHTML += "inBuffer[3] = readReverseBase64();";
+  resultHTML += "result += ntos((((inBuffer[0] << 2) & 0xff)| inBuffer[1] >> 4));";
+  resultHTML += "if (inBuffer[2] != END_OF_INPUT){";
+  resultHTML += "result +=  ntos((((inBuffer[1] << 4) & 0xff)| inBuffer[2] >> 2));";
+  resultHTML += "if (inBuffer[3] != END_OF_INPUT){";
+  resultHTML += "result +=  ntos((((inBuffer[2] << 6)  & 0xff) | inBuffer[3]));";
+  resultHTML += "} else { done = true; }";
+  resultHTML += "} else { done = true; } }";
+  resultHTML += "return result; }";
+
+  this->parseScene(renderers, "1234567890", VTK_PARSEALL);
+  const char* metadata = this->GenerateExportMetadata();
+  resultHTML += "var metadata = '" + std::string(metadata) + "';";
+  resultHTML += "var object = [";
+  for (int i = 0; i < this->GetNumberOfObjects(); i++)
+  {
+    std::string test;
+    int size = 0;
+
+    vtkWebGLObject* obj = this->GetWebGLObject(i);
+    if (obj->isVisible())
+    {
+      for (int j = 0; j < obj->GetNumberOfParts(); j++)
+      {
+        unsigned char* output = new unsigned char[obj->GetBinarySize(j) * 2];
+        size =
+          vtkBase64Utilities::Encode(obj->GetBinaryData(j), obj->GetBinarySize(j), output, false);
+        test = std::string((const char*)output, size);
+        resultHTML += "'" + test + "',\n";
+        delete[] output;
+      }
+    }
+  }
+  resultHTML += "''];";
+
+  resultHTML += webglRenderer;
+  resultHTML += glMatrix;
+
+  resultHTML += "</script></html>";
+
+  vtksys::ofstream file;
+  file.open(path.c_str());
+  file << resultHTML;
+  file.close();
+}
+
+//------------------------------------------------------------------------------
+void vtkWebGLExporter::ComputeMD5(const unsigned char* content, int size, std::string& hash)
+{
+  unsigned char digest[16];
+  char md5Hash[33];
+  md5Hash[32] = '\0';
+
+  vtksysMD5* md5 = vtksysMD5_New();
+  vtksysMD5_Initialize(md5);
+  vtksysMD5_Append(md5, content, size);
+  vtksysMD5_Finalize(md5, digest);
+  vtksysMD5_DigestToHex(digest, md5Hash);
+  vtksysMD5_Delete(md5);
+
+  hash = md5Hash;
+}
+VTK_ABI_NAMESPACE_END
diff --git a/Web/WebGLExporter/vtkWebGLExporter.h b/Web/WebGLExporter/vtkWebGLExporter.h
new file mode 100644 (file)
index 0000000..eeae073
--- /dev/null
@@ -0,0 +1,101 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+/**
+ * @class   vtkWebGLExporter
+ * @brief   vtkWebGLExporter export the data of the scene to be used in the WebGL.
+ */
+
+#ifndef vtkWebGLExporter_h
+#define vtkWebGLExporter_h
+
+#include "vtkObject.h"
+#include "vtkWebGLExporterModule.h" // needed for export macro
+
+VTK_ABI_NAMESPACE_BEGIN
+class vtkActor;
+class vtkActor2D;
+class vtkCellData;
+class vtkMapper;
+class vtkPointData;
+class vtkPolyData;
+class vtkRenderer;
+class vtkRendererCollection;
+class vtkTriangleFilter;
+class vtkWebGLObject;
+class vtkWebGLPolyData;
+
+VTK_ABI_NAMESPACE_END
+
+#include <string> // needed for internal structure
+
+VTK_ABI_NAMESPACE_BEGIN
+typedef enum
+{
+  VTK_ONLYCAMERA = 0,
+  VTK_ONLYWIDGET = 1,
+  VTK_PARSEALL = 2
+} VTKParseType;
+
+class VTKWEBGLEXPORTER_EXPORT vtkWebGLExporter : public vtkObject
+{
+public:
+  static vtkWebGLExporter* New();
+  vtkTypeMacro(vtkWebGLExporter, vtkObject);
+  void PrintSelf(ostream& os, vtkIndent indent) override;
+
+  ///@{
+  /**
+   * Get all the needed information from the vtkRenderer
+   */
+  void parseScene(vtkRendererCollection* renderers, const char* viewId, int parseType);
+  // Generate and return the Metadata
+  void exportStaticScene(vtkRendererCollection* renderers, int width, int height, std::string path);
+  const char* GenerateMetadata();
+  const char* GetId();
+  vtkWebGLObject* GetWebGLObject(int index);
+  int GetNumberOfObjects();
+  bool hasChanged();
+  void SetCenterOfRotation(float a1, float a2, float a3);
+  void SetMaxAllowedSize(int mesh, int lines);
+  void SetMaxAllowedSize(int size);
+  ///@}
+
+  static void ComputeMD5(const unsigned char* content, int size, std::string& hash);
+
+protected:
+  vtkWebGLExporter();
+  ~vtkWebGLExporter() override;
+
+  void parseRenderer(vtkRenderer* render, const char* viewId, bool onlyWidget, void* mapTime);
+  void generateRendererData(vtkRendererCollection* renderers, const char* viewId);
+  void parseActor(
+    vtkActor* actor, vtkMTimeType actorTime, size_t rendererId, int layer, bool isWidget);
+  void parseActor2D(
+    vtkActor2D* actor, vtkMTimeType actorTime, size_t renderId, int layer, bool isWidget);
+  const char* GenerateExportMetadata();
+
+  // Get the dataset from the mapper
+  vtkTriangleFilter* GetPolyData(vtkMapper* mapper, vtkMTimeType& dataMTime);
+
+  vtkTriangleFilter* TriangleFilter;  // Last Polygon Dataset Parse
+  double CameraLookAt[10];            // Camera Look At (fov, position[3], up[3], eye[3])
+  bool GradientBackground;            // If the scene use a gradient background
+  double Background1[3];              // Background color of the rendering screen (RGB)
+  double Background2[3];              // Second background color
+  double SceneSize[3];                // Size of the bounding box of the scene
+  std::string SceneId;                // Id of the parsed scene
+  float CenterOfRotation[3];          // Center Of Rotation
+  int meshObjMaxSize, lineObjMaxSize; // Max size of object allowed (faces)
+  std::string renderersMetaData;
+  bool hasWidget;
+
+private:
+  vtkWebGLExporter(const vtkWebGLExporter&) = delete;
+  void operator=(const vtkWebGLExporter&) = delete;
+
+  class vtkInternal;
+  vtkInternal* Internal;
+};
+
+VTK_ABI_NAMESPACE_END
+#endif
diff --git a/Web/WebGLExporter/vtkWebGLObject.cxx b/Web/WebGLExporter/vtkWebGLObject.cxx
new file mode 100644 (file)
index 0000000..70891a4
--- /dev/null
@@ -0,0 +1,201 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include "vtkWebGLObject.h"
+
+#include "vtkMatrix4x4.h"
+#include "vtkObjectFactory.h"
+#include "vtkUnsignedCharArray.h"
+
+#include <algorithm>
+
+VTK_ABI_NAMESPACE_BEGIN
+vtkStandardNewMacro(vtkWebGLObject);
+VTK_ABI_NAMESPACE_END
+#include <map>
+#include <vector>
+
+//------------------------------------------------------------------------------
+VTK_ABI_NAMESPACE_BEGIN
+vtkWebGLObject::vtkWebGLObject()
+{
+  this->iswireframeMode = false;
+  this->hasChanged = false;
+  this->webGlType = wTRIANGLES;
+  this->hasTransparency = false;
+  this->iswidget = false;
+  this->interactAtServer = false;
+}
+
+//------------------------------------------------------------------------------
+vtkWebGLObject::~vtkWebGLObject() = default;
+
+//------------------------------------------------------------------------------
+std::string vtkWebGLObject::GetId()
+{
+  return this->id;
+}
+
+//------------------------------------------------------------------------------
+void vtkWebGLObject::SetId(const std::string& i)
+{
+  this->id = i;
+}
+
+//------------------------------------------------------------------------------
+void vtkWebGLObject::SetType(WebGLObjectTypes t)
+{
+  this->webGlType = t;
+}
+
+//------------------------------------------------------------------------------
+void vtkWebGLObject::SetTransformationMatrix(vtkMatrix4x4* m)
+{
+  for (int i = 0; i < 16; i++)
+    this->Matrix[i] = m->GetElement(i / 4, i % 4);
+}
+
+//------------------------------------------------------------------------------
+std::string vtkWebGLObject::GetMD5()
+{
+  return this->MD5;
+}
+
+//------------------------------------------------------------------------------
+void vtkWebGLObject::PrintSelf(ostream& os, vtkIndent indent)
+{
+  this->Superclass::PrintSelf(os, indent);
+}
+
+//------------------------------------------------------------------------------
+bool vtkWebGLObject::HasChanged()
+{
+  return this->hasChanged;
+}
+
+//------------------------------------------------------------------------------
+void vtkWebGLObject::SetWireframeMode(bool wireframe)
+{
+  this->iswireframeMode = wireframe;
+}
+
+//------------------------------------------------------------------------------
+bool vtkWebGLObject::isWireframeMode()
+{
+  return this->iswireframeMode;
+}
+
+//------------------------------------------------------------------------------
+void vtkWebGLObject::SetVisibility(bool vis)
+{
+  this->isvisible = vis;
+}
+
+//------------------------------------------------------------------------------
+bool vtkWebGLObject::isVisible()
+{
+  return this->isvisible;
+}
+
+//------------------------------------------------------------------------------
+void vtkWebGLObject::SetHasTransparency(bool t)
+{
+  this->hasTransparency = t;
+}
+
+//------------------------------------------------------------------------------
+void vtkWebGLObject::SetIsWidget(bool w)
+{
+  this->iswidget = w;
+}
+
+//------------------------------------------------------------------------------
+bool vtkWebGLObject::isWidget()
+{
+  return this->iswidget;
+}
+
+//------------------------------------------------------------------------------
+bool vtkWebGLObject::HasTransparency()
+{
+  return this->hasTransparency;
+}
+
+//------------------------------------------------------------------------------
+void vtkWebGLObject::SetRendererId(size_t i)
+{
+  this->rendererId = i;
+}
+
+//------------------------------------------------------------------------------
+size_t vtkWebGLObject::GetRendererId()
+{
+  return this->rendererId;
+}
+
+//------------------------------------------------------------------------------
+void vtkWebGLObject::SetLayer(int l)
+{
+  this->layer = l;
+}
+
+//------------------------------------------------------------------------------
+int vtkWebGLObject::GetLayer()
+{
+  return this->layer;
+}
+
+//------------------------------------------------------------------------------
+bool vtkWebGLObject::InteractAtServer()
+{
+  return this->interactAtServer;
+}
+
+//------------------------------------------------------------------------------
+void vtkWebGLObject::SetInteractAtServer(bool i)
+{
+  this->interactAtServer = i;
+}
+
+//------------------------------------------------------------------------------
+void vtkWebGLObject::GetBinaryData(int part, vtkUnsignedCharArray* buffer)
+{
+  if (!buffer)
+  {
+    vtkErrorMacro("Buffer must not be nullptr.");
+    return;
+  }
+
+  const int binarySize = this->GetBinarySize(part);
+  const unsigned char* binaryData = this->GetBinaryData(part);
+
+  buffer->SetNumberOfComponents(1);
+  buffer->SetNumberOfTuples(binarySize);
+
+  if (binarySize)
+  {
+    std::copy(binaryData, binaryData + binarySize, buffer->GetPointer(0));
+  }
+}
+
+//------------------------------------------------------------------------------
+void vtkWebGLObject::GenerateBinaryData()
+{
+  this->hasChanged = false;
+}
+//------------------------------------------------------------------------------
+unsigned char* vtkWebGLObject::GetBinaryData(int vtkNotUsed(part))
+{
+  return nullptr;
+}
+//------------------------------------------------------------------------------
+int vtkWebGLObject::GetBinarySize(int vtkNotUsed(part))
+{
+  return 0;
+}
+//------------------------------------------------------------------------------
+int vtkWebGLObject::GetNumberOfParts()
+{
+  return 0;
+}
+VTK_ABI_NAMESPACE_END
diff --git a/Web/WebGLExporter/vtkWebGLObject.h b/Web/WebGLExporter/vtkWebGLObject.h
new file mode 100644 (file)
index 0000000..4bd67b9
--- /dev/null
@@ -0,0 +1,92 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+/**
+ * @class   vtkWebGLObject
+ * @brief   vtkWebGLObject represent and manipulate an WebGL object and its data.
+ */
+
+#ifndef vtkWebGLObject_h
+#define vtkWebGLObject_h
+
+#include "vtkObject.h"
+#include "vtkWebGLExporterModule.h" // needed for export macro
+
+#include <string> // needed for ID and md5 storing
+
+VTK_ABI_NAMESPACE_BEGIN
+class vtkMatrix4x4;
+class vtkUnsignedCharArray;
+
+enum WebGLObjectTypes
+{
+  wPOINTS = 0,
+  wLINES = 1,
+  wTRIANGLES = 2
+};
+
+class VTKWEBGLEXPORTER_EXPORT vtkWebGLObject : public vtkObject
+{
+public:
+  static vtkWebGLObject* New();
+  vtkTypeMacro(vtkWebGLObject, vtkObject);
+  void PrintSelf(ostream& os, vtkIndent indent) override;
+
+  virtual void GenerateBinaryData();
+  virtual unsigned char* GetBinaryData(int part);
+  virtual int GetBinarySize(int part);
+  virtual int GetNumberOfParts();
+
+  /**
+   * This is a wrapper friendly method for access the binary data.
+   * The binary data for the requested part will be copied into the
+   * given vtkUnsignedCharArray.
+   */
+  void GetBinaryData(int part, vtkUnsignedCharArray* buffer);
+
+  void SetLayer(int l);
+  void SetRendererId(size_t i);
+  void SetId(const std::string& i);
+  void SetWireframeMode(bool wireframe);
+  void SetVisibility(bool vis);
+  void SetTransformationMatrix(vtkMatrix4x4* m);
+  void SetIsWidget(bool w);
+  void SetHasTransparency(bool t);
+  void SetInteractAtServer(bool i);
+  void SetType(WebGLObjectTypes t);
+  bool isWireframeMode();
+  bool isVisible();
+  bool HasChanged();
+  bool isWidget();
+  bool HasTransparency();
+  bool InteractAtServer();
+
+  std::string GetMD5();
+  std::string GetId();
+
+  size_t GetRendererId();
+  int GetLayer();
+
+protected:
+  vtkWebGLObject();
+  ~vtkWebGLObject() override;
+
+  float Matrix[16];
+  size_t rendererId;
+  int layer;      // Renderer Layer
+  std::string id; // Id of the object
+  std::string MD5;
+  bool hasChanged;
+  bool iswireframeMode;
+  bool isvisible;
+  WebGLObjectTypes webGlType;
+  bool hasTransparency;
+  bool iswidget;
+  bool interactAtServer;
+
+private:
+  vtkWebGLObject(const vtkWebGLObject&) = delete;
+  void operator=(const vtkWebGLObject&) = delete;
+};
+
+VTK_ABI_NAMESPACE_END
+#endif
diff --git a/Web/WebGLExporter/vtkWebGLPolyData.cxx b/Web/WebGLExporter/vtkWebGLPolyData.cxx
new file mode 100644 (file)
index 0000000..7dae8a2
--- /dev/null
@@ -0,0 +1,783 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include "vtkWebGLPolyData.h"
+
+#include "vtkActor.h"
+#include "vtkCell.h"
+#include "vtkCellArray.h"
+#include "vtkCellData.h"
+#include "vtkCompositeDataGeometryFilter.h"
+#include "vtkCompositeDataSet.h"
+#include "vtkGenericCell.h"
+#include "vtkIdTypeArray.h"
+#include "vtkMapper.h"
+#include "vtkMatrix4x4.h"
+#include "vtkObjectFactory.h"
+#include "vtkPointData.h"
+#include "vtkPoints.h"
+#include "vtkPolyDataNormals.h"
+#include "vtkProperty.h"
+#include "vtkScalarsToColors.h"
+#include "vtkSmartPointer.h"
+#include "vtkTriangleFilter.h"
+#include "vtkUnsignedCharArray.h"
+#include "vtkWebGLDataSet.h"
+#include "vtkWebGLExporter.h"
+#include "vtkWebGLObject.h"
+
+#include <map>
+#include <sstream>
+#include <string>
+#include <vector>
+
+VTK_ABI_NAMESPACE_BEGIN
+vtkStandardNewMacro(vtkWebGLPolyData);
+//*****************************************************************************
+class vtkWebGLPolyData::vtkInternal
+{
+public:
+  std::vector<vtkWebGLDataSet*> Parts;
+  std::map<long int, short> IndexMap;
+};
+//*****************************************************************************
+
+vtkWebGLPolyData::vtkWebGLPolyData()
+{
+  this->webGlType = wTRIANGLES;
+  this->iswidget = false;
+  this->Internal = new vtkInternal();
+}
+
+vtkWebGLPolyData::~vtkWebGLPolyData()
+{
+  vtkWebGLDataSet* obj;
+  while (!this->Internal->Parts.empty())
+  {
+    obj = this->Internal->Parts.back();
+    this->Internal->Parts.pop_back();
+    obj->Delete();
+  }
+  delete this->Internal;
+}
+
+void vtkWebGLPolyData::SetMesh(float* _vertices, int _numberOfVertices, int* _index,
+  int _numberOfIndexes, float* _normals, unsigned char* _colors, float* _tcoords, int maxSize)
+{
+  this->webGlType = wTRIANGLES;
+
+  vtkWebGLDataSet* obj;
+  while (!this->Internal->Parts.empty())
+  {
+    obj = this->Internal->Parts.back();
+    this->Internal->Parts.pop_back();
+    obj->Delete();
+  }
+
+  short* index;
+  int div = maxSize * 3;
+  if (_numberOfVertices < div)
+  {
+    index = new short[_numberOfIndexes];
+    for (int i = 0; i < _numberOfIndexes; i++)
+      index[i] = (short)_index[i];
+
+    obj = vtkWebGLDataSet::New();
+    obj->SetVertices(_vertices, _numberOfVertices);
+    obj->SetIndexes(index, _numberOfIndexes);
+    obj->SetNormals(_normals);
+    obj->SetColors(_colors);
+    obj->SetMatrix(this->Matrix);
+    this->Internal->Parts.push_back(obj);
+  }
+  else
+  {
+    int total = _numberOfIndexes;
+    int curr = 0;
+    int size = 0;
+
+    while (curr < total)
+    {
+      if (div + curr > total)
+        size = total - curr;
+      else
+        size = div;
+
+      float* vertices = new float[size * 3];
+      float* normals = new float[size * 3];
+      unsigned char* colors = new unsigned char[size * 4];
+      short* indexes = new short[size];
+      float* tcoord = nullptr;
+      if (_tcoords)
+        tcoord = new float[size * 2];
+
+      this->Internal->IndexMap.clear();
+      int count = 0;
+      for (int j = 0; j < size; j++)
+      {
+        int ind = _index[curr + j];
+        if (this->Internal->IndexMap.find(ind) == this->Internal->IndexMap.end())
+        {
+          vertices[count * 3 + 0] = _vertices[ind * 3 + 0];
+          vertices[count * 3 + 1] = _vertices[ind * 3 + 1];
+          vertices[count * 3 + 2] = _vertices[ind * 3 + 2];
+
+          normals[count * 3 + 0] = _normals[ind * 3 + 0];
+          normals[count * 3 + 1] = _normals[ind * 3 + 1];
+          normals[count * 3 + 2] = _normals[ind * 3 + 2];
+
+          colors[count * 4 + 0] = _colors[ind * 4 + 0];
+          colors[count * 4 + 1] = _colors[ind * 4 + 1];
+          colors[count * 4 + 2] = _colors[ind * 4 + 2];
+          colors[count * 4 + 3] = _colors[ind * 4 + 3];
+
+          if (_tcoords)
+          {
+            tcoord[count * 2 + 0] = _tcoords[ind * 2 + 0];
+            tcoord[count * 2 + 1] = _tcoords[ind * 2 + 1];
+          }
+          this->Internal->IndexMap[ind] = count;
+          indexes[j] = count++;
+        }
+        else
+        {
+          indexes[j] = this->Internal->IndexMap[ind];
+        }
+      }
+      curr += size;
+      float* v = new float[count * 3];
+      memcpy(v, vertices, count * 3 * sizeof(float));
+      delete[] vertices;
+      float* n = new float[count * 3];
+      memcpy(n, normals, count * 3 * sizeof(float));
+      delete[] normals;
+      unsigned char* c = new unsigned char[count * 4];
+      memcpy(c, colors, count * 4);
+      delete[] colors;
+      obj = vtkWebGLDataSet::New();
+      obj->SetVertices(v, count);
+      obj->SetIndexes(indexes, size);
+      obj->SetNormals(n);
+      obj->SetColors(c);
+      if (_tcoords)
+      {
+        float* tc = new float[count * 2];
+        memcpy(tc, tcoord, count * 2 * sizeof(float));
+        delete[] tcoord;
+        obj->SetTCoords(tc);
+      }
+      obj->SetMatrix(this->Matrix);
+      this->Internal->Parts.push_back(obj);
+    }
+
+    delete[] _vertices;
+    delete[] _index;
+    delete[] _normals;
+    delete[] _colors;
+    delete[] _tcoords;
+  }
+}
+
+void vtkWebGLPolyData::SetLine(float* _points, int _numberOfPoints, int* _index, int _numberOfIndex,
+  unsigned char* _colors, int maxSize)
+{
+  this->webGlType = wLINES;
+
+  vtkWebGLDataSet* obj;
+  while (!this->Internal->Parts.empty())
+  {
+    obj = this->Internal->Parts.back();
+    this->Internal->Parts.pop_back();
+    obj->Delete();
+  }
+
+  short* index;
+  int div = maxSize * 2;
+  if (_numberOfPoints < div)
+  {
+    index = new short[_numberOfIndex];
+    for (int i = 0; i < _numberOfIndex; i++)
+      index[i] = (short)((unsigned int)_index[i]);
+    obj = vtkWebGLDataSet::New();
+    obj->SetPoints(_points, _numberOfPoints);
+    obj->SetIndexes(index, _numberOfIndex);
+    obj->SetColors(_colors);
+    obj->SetMatrix(this->Matrix);
+    this->Internal->Parts.push_back(obj);
+  }
+  else
+  {
+    int total = _numberOfIndex;
+    int curr = 0;
+    int size = 0;
+
+    while (curr < total)
+    {
+      if (div + curr > total)
+        size = total - curr;
+      else
+        size = div;
+
+      float* points = new float[size * 3];
+      unsigned char* colors = new unsigned char[size * 4];
+      short* indexes = new short[size];
+
+      for (int j = 0; j < size; j++)
+      {
+        indexes[j] = j;
+
+        points[j * 3 + 0] = _points[_index[curr + j] * 3 + 0];
+        points[j * 3 + 1] = _points[_index[curr + j] * 3 + 1];
+        points[j * 3 + 2] = _points[_index[curr + j] * 3 + 2];
+
+        colors[j * 4 + 0] = _colors[_index[curr + j] * 4 + 0];
+        colors[j * 4 + 1] = _colors[_index[curr + j] * 4 + 1];
+        colors[j * 4 + 2] = _colors[_index[curr + j] * 4 + 2];
+        colors[j * 4 + 3] = _colors[_index[curr + j] * 4 + 3];
+      }
+      curr += size;
+      obj = vtkWebGLDataSet::New();
+      obj->SetPoints(points, size);
+      obj->SetIndexes(indexes, size);
+      obj->SetColors(colors);
+      obj->SetMatrix(this->Matrix);
+      this->Internal->Parts.push_back(obj);
+    }
+    delete[] _points;
+    delete[] _index;
+    delete[] _colors;
+  }
+}
+
+void vtkWebGLPolyData::SetTransformationMatrix(vtkMatrix4x4* m)
+{
+  this->Superclass::SetTransformationMatrix(m);
+  for (size_t i = 0; i < this->Internal->Parts.size(); i++)
+  {
+    this->Internal->Parts[i]->SetMatrix(this->Matrix);
+  }
+}
+
+unsigned char* vtkWebGLPolyData::GetBinaryData(int part)
+{
+  this->hasChanged = false;
+  vtkWebGLDataSet* obj = this->Internal->Parts[part];
+  return obj->GetBinaryData();
+}
+
+int vtkWebGLPolyData::GetBinarySize(int part)
+{
+  vtkWebGLDataSet* obj = this->Internal->Parts[part];
+  return obj->GetBinarySize();
+}
+
+void vtkWebGLPolyData::GenerateBinaryData()
+{
+  vtkWebGLDataSet* obj;
+  this->hasChanged = false;
+  std::stringstream ss;
+  for (size_t i = 0; i < this->Internal->Parts.size(); i++)
+  {
+    obj = this->Internal->Parts[i];
+    obj->GenerateBinaryData();
+    ss << obj->GetMD5();
+  }
+  if (!this->Internal->Parts.empty())
+  {
+    std::string localMD5;
+    vtkWebGLExporter::ComputeMD5(
+      (const unsigned char*)ss.str().c_str(), static_cast<int>(ss.str().size()), localMD5);
+    this->hasChanged = this->MD5 != localMD5;
+    this->MD5 = localMD5;
+  }
+  else
+    cout << "Warning: GenerateBinaryData() @ vtkWebGLObject: This isn\'t supposed to happen.";
+}
+
+int vtkWebGLPolyData::GetNumberOfParts()
+{
+  return static_cast<int>(this->Internal->Parts.size());
+}
+
+void vtkWebGLPolyData::PrintSelf(ostream& os, vtkIndent indent)
+{
+  this->Superclass::PrintSelf(os, indent);
+}
+
+void vtkWebGLPolyData::GetLinesFromPolygon(
+  vtkMapper* mapper, vtkActor* actor, int lineMaxSize, double* edgeColor)
+{
+  vtkWebGLPolyData* object = this;
+  vtkDataSet* dataset = nullptr;
+  vtkSmartPointer<vtkDataSet> tempDS;
+  vtkDataObject* dObj = mapper->GetInputDataObject(0, 0);
+  vtkCompositeDataSet* cd = vtkCompositeDataSet::SafeDownCast(dObj);
+  if (cd)
+  {
+    vtkCompositeDataGeometryFilter* gf = vtkCompositeDataGeometryFilter::New();
+    gf->SetInputData(cd);
+    gf->Update();
+    tempDS = gf->GetOutput();
+    gf->Delete();
+    dataset = tempDS;
+  }
+  else
+  {
+    dataset = mapper->GetInput();
+  }
+
+  int np = 0;
+  int size = 0;
+  for (int i = 0; i < dataset->GetNumberOfCells(); i++)
+    size += dataset->GetCell(i)->GetNumberOfPoints();
+
+  float* points = new float[size * 3];
+  unsigned char* color = new unsigned char[size * 4];
+  int* index = new int[size * 2];
+  double* point;
+  int pos = 0;
+
+  vtkScalarsToColors* table = mapper->GetLookupTable();
+  vtkDataArray* array;
+  if (mapper->GetScalarMode() == VTK_SCALAR_MODE_USE_CELL_FIELD_DATA)
+  {
+    vtkCellData* celldata = dataset->GetCellData();
+    if (actor->GetMapper()->GetArrayAccessMode() == VTK_GET_ARRAY_BY_ID)
+      array = celldata->GetArray(actor->GetMapper()->GetArrayId());
+    else
+      array = celldata->GetArray(actor->GetMapper()->GetArrayName());
+  }
+  else
+  {
+    vtkPointData* pointdata = dataset->GetPointData();
+    if (actor->GetMapper()->GetArrayAccessMode() == VTK_GET_ARRAY_BY_ID)
+      array = pointdata->GetArray(actor->GetMapper()->GetArrayId());
+    else
+      array = pointdata->GetArray(actor->GetMapper()->GetArrayName());
+  }
+
+  int colorComponent = table->GetVectorComponent();
+  int numberOfComponents = 0;
+  if (array != nullptr)
+    numberOfComponents = array->GetNumberOfComponents();
+  int mode = table->GetVectorMode();
+  double mag = 0, rgb[3];
+  int curr = 0;
+  for (int i = 0; i < dataset->GetNumberOfCells(); i++)
+  {
+    vtkCell* cell = dataset->GetCell(i);
+    int b = pos;
+    np = dataset->GetCell(i)->GetNumberOfPoints();
+    for (int j = 0; j < np; j++)
+    {
+      point = cell->GetPoints()->GetPoint(j);
+      points[curr * 3 + j * 3 + 0] = point[0];
+      points[curr * 3 + j * 3 + 1] = point[1];
+      points[curr * 3 + j * 3 + 2] = point[2];
+
+      index[curr * 2 + j * 2 + 0] = pos++;
+      index[curr * 2 + j * 2 + 1] = pos;
+      if (j == np - 1)
+        index[curr * 2 + j * 2 + 1] = b;
+
+      vtkIdType pointId = cell->GetPointIds()->GetId(j);
+      if (numberOfComponents == 0)
+      {
+        actor->GetProperty()->GetColor(rgb);
+      }
+      else
+      {
+        switch (mode)
+        {
+          case vtkScalarsToColors::MAGNITUDE:
+            mag = 0;
+            for (int w = 0; w < numberOfComponents; w++)
+              mag += array->GetComponent(pointId, w) * array->GetComponent(pointId, w);
+            mag = sqrt(mag);
+            table->GetColor(mag, &rgb[0]);
+            break;
+          case vtkScalarsToColors::COMPONENT:
+            mag = array->GetComponent(pointId, colorComponent);
+            table->GetColor(mag, &rgb[0]);
+            break;
+          case vtkScalarsToColors::RGBCOLORS:
+            array->GetTuple(pointId, &rgb[0]);
+            break;
+        }
+      }
+      if (edgeColor != nullptr)
+        memcpy(rgb, edgeColor, sizeof(double) * 3);
+      color[curr * 4 + j * 4 + 0] = (unsigned char)((int)(rgb[0] * 255));
+      color[curr * 4 + j * 4 + 1] = (unsigned char)((int)(rgb[1] * 255));
+      color[curr * 4 + j * 4 + 2] = (unsigned char)((int)(rgb[2] * 255));
+      color[curr * 4 + j * 4 + 3] = (unsigned char)255;
+    }
+    curr += np;
+  }
+  object->SetLine(points, size, index, size * 2, color, lineMaxSize);
+}
+
+void vtkWebGLPolyData::GetLines(vtkTriangleFilter* polydata, vtkActor* actor, int lineMaxSize)
+{
+  vtkWebGLPolyData* object = this;
+  vtkCellArray* lines = polydata->GetOutput(0)->GetLines();
+
+  // Index
+  // Array of 3 Values. [#number of index, i1, i2]
+  // Discarting the first value
+  vtkDataArray* conn = lines->GetConnectivityArray();
+  const vtkIdType connSize = conn->GetNumberOfValues();
+  int* index = new int[static_cast<size_t>(connSize)];
+  for (vtkIdType i = 0; i < connSize; ++i)
+  {
+    index[i] = static_cast<int>(conn->GetComponent(i, 0));
+  }
+  // Point
+  double point[3];
+  float* points = new float[polydata->GetOutput(0)->GetNumberOfPoints() * 3];
+  for (int i = 0; i < polydata->GetOutput(0)->GetNumberOfPoints(); i++)
+  {
+    polydata->GetOutput(0)->GetPoint(i, point);
+    points[i * 3 + 0] = point[0];
+    points[i * 3 + 1] = point[1];
+    points[i * 3 + 2] = point[2];
+  }
+  // Colors
+  unsigned char* color = new unsigned char[polydata->GetOutput(0)->GetNumberOfPoints() * 4];
+  this->GetColorsFromPolyData(color, polydata->GetOutput(0), actor);
+
+  object->SetLine(points, polydata->GetOutput(0)->GetNumberOfPoints(), index,
+    static_cast<int>(connSize), color, lineMaxSize);
+}
+
+void vtkWebGLPolyData::SetPoints(
+  float* points, int numberOfPoints, unsigned char* colors, int maxSize)
+{
+  this->webGlType = wPOINTS;
+
+  // Delete Old Objects
+  vtkWebGLDataSet* obj;
+  while (!this->Internal->Parts.empty())
+  {
+    obj = this->Internal->Parts.back();
+    this->Internal->Parts.pop_back();
+    obj->Delete();
+  }
+
+  // Create new objs
+  int numObjs = (numberOfPoints / maxSize) + 1;
+  int offset = 0;
+  int size = 0;
+  for (int i = 0; i < numObjs; i++)
+  {
+    size = numberOfPoints - offset;
+    if (size > maxSize)
+      size = maxSize;
+
+    float* _points = new float[size * 3];
+    unsigned char* _colors = new unsigned char[size * 4];
+    memcpy(_points, &points[offset * 3], size * 3 * sizeof(float));
+    memcpy(_colors, &colors[offset * 4], size * 4 * sizeof(unsigned char));
+
+    obj = vtkWebGLDataSet::New();
+    obj->SetPoints(_points, size);
+    obj->SetColors(_colors);
+    obj->SetType(wPOINTS);
+    obj->SetMatrix(this->Matrix);
+    this->Internal->Parts.push_back(obj);
+
+    offset += size;
+  }
+
+  delete[] points;
+  delete[] colors;
+}
+
+void vtkWebGLPolyData::GetPoints(vtkTriangleFilter* polydata, vtkActor* actor, int maxSize)
+{
+  vtkWebGLPolyData* object = this;
+
+  // Points
+  double point[3];
+  float* points = new float[polydata->GetOutput(0)->GetNumberOfPoints() * 3];
+  for (int i = 0; i < polydata->GetOutput(0)->GetNumberOfPoints(); i++)
+  {
+    polydata->GetOutput(0)->GetPoint(i, point);
+    points[i * 3 + 0] = point[0];
+    points[i * 3 + 1] = point[1];
+    points[i * 3 + 2] = point[2];
+  }
+  // Colors
+  unsigned char* colors = new unsigned char[polydata->GetOutput(0)->GetNumberOfPoints() * 4];
+  this->GetColorsFromPolyData(colors, polydata->GetOutput(0), actor);
+
+  object->SetPoints(points, polydata->GetOutput(0)->GetNumberOfPoints(), colors, maxSize);
+}
+
+void vtkWebGLPolyData::GetColorsFromPolyData(
+  unsigned char* color, vtkPolyData* polydata, vtkActor* actor)
+{
+  int celldata;
+  vtkDataArray* array = vtkAbstractMapper::GetScalars(polydata, actor->GetMapper()->GetScalarMode(),
+    actor->GetMapper()->GetArrayAccessMode(), actor->GetMapper()->GetArrayId(),
+    actor->GetMapper()->GetArrayName(), celldata);
+  if (actor->GetMapper()->GetScalarVisibility() && array != nullptr)
+  {
+    vtkScalarsToColors* table = actor->GetMapper()->GetLookupTable();
+
+    vtkUnsignedCharArray* cor =
+      table->MapScalars(array, table->GetVectorMode(), table->GetVectorComponent());
+    memcpy(color, cor->GetPointer(0), polydata->GetNumberOfPoints() * 4);
+    cor->Delete();
+  }
+  else
+  {
+    for (int i = 0; i < polydata->GetNumberOfPoints(); i++)
+    {
+      color[i * 4 + 0] = (unsigned char)255;
+      color[i * 4 + 1] = (unsigned char)255;
+      color[i * 4 + 2] = (unsigned char)255;
+      color[i * 4 + 3] = (unsigned char)255;
+    }
+  }
+}
+
+void vtkWebGLPolyData::GetPolygonsFromPointData(
+  vtkTriangleFilter* polydata, vtkActor* actor, int maxSize)
+{
+  vtkWebGLPolyData* object = this;
+
+  vtkPolyDataNormals* polynormals = vtkPolyDataNormals::New();
+  polynormals->SetInputConnection(polydata->GetOutputPort(0));
+  polynormals->Update();
+
+  vtkPolyData* data = polynormals->GetOutput();
+
+  vtkCellArray* poly = data->GetPolys();
+  vtkPointData* point = data->GetPointData();
+  vtkNew<vtkIdTypeArray> ndata;
+  poly->ExportLegacyFormat(ndata);
+  vtkDataSetAttributes* attr = (vtkDataSetAttributes*)point;
+
+  // Vertices
+  float* vertices = new float[data->GetNumberOfPoints() * 3];
+  for (int i = 0; i < data->GetNumberOfPoints() * 3; i++)
+    vertices[i] = data->GetPoint(i / 3)[i % 3];
+  // Index
+  // ndata contain 4 values for the normal: [number of values per index, index[3]]
+  // We don't need the first value
+  int* indexes = new int[ndata->GetSize() * 3 / 4];
+  for (int i = 0; i < ndata->GetSize(); i++)
+    if (i % 4 != 0)
+      indexes[i * 3 / 4] = ndata->GetValue(i);
+  // Normal
+  float* normal = new float[attr->GetNormals()->GetSize()];
+  for (int i = 0; i < attr->GetNormals()->GetSize(); i++)
+    normal[i] = attr->GetNormals()->GetComponent(0, i);
+  // Colors
+  unsigned char* color = new unsigned char[data->GetNumberOfPoints() * 4];
+  this->GetColorsFromPointData(color, point, data, actor);
+  // TCoord
+  float* tcoord = nullptr;
+  if (attr->GetTCoords())
+  {
+    tcoord = new float[attr->GetTCoords()->GetSize()];
+    for (int i = 0; i < attr->GetTCoords()->GetSize(); i++)
+      tcoord[i] = attr->GetTCoords()->GetComponent(0, i);
+  }
+
+  object->SetMesh(vertices, data->GetNumberOfPoints(), indexes, ndata->GetSize() * 3 / 4, normal,
+    color, tcoord, maxSize);
+  polynormals->Delete();
+}
+
+void vtkWebGLPolyData::GetPolygonsFromCellData(
+  vtkTriangleFilter* polydata, vtkActor* actor, int maxSize)
+{
+  vtkWebGLPolyData* object = this;
+
+  vtkPolyDataNormals* polynormals = vtkPolyDataNormals::New();
+  polynormals->SetInputConnection(polydata->GetOutputPort(0));
+  polynormals->Update();
+
+  vtkPolyData* data = polynormals->GetOutput();
+  vtkCellData* celldata = data->GetCellData();
+
+  vtkDataArray* array;
+  if (actor->GetMapper()->GetArrayAccessMode() == VTK_GET_ARRAY_BY_ID)
+    array = celldata->GetArray(actor->GetMapper()->GetArrayId());
+  else
+    array = celldata->GetArray(actor->GetMapper()->GetArrayName());
+  vtkScalarsToColors* table = actor->GetMapper()->GetLookupTable();
+  int colorComponent = table->GetVectorComponent();
+  int mode = table->GetVectorMode();
+
+  float* vertices = new float[data->GetNumberOfCells() * 3 * 3];
+  float* normals = new float[data->GetNumberOfCells() * 3 * 3];
+  unsigned char* colors = new unsigned char[data->GetNumberOfCells() * 3 * 4];
+  int* indexes = new int[data->GetNumberOfCells() * 3 * 3];
+
+  vtkGenericCell* cell = vtkGenericCell::New();
+  double tuple[3], normal[3], color[3];
+  color[0] = 1.0;
+  color[1] = 1.0;
+  color[2] = 1.0;
+  vtkPoints* points;
+  int aux;
+  double mag, alpha = 1.0;
+  int numberOfComponents = 0;
+  if (array)
+    numberOfComponents = array->GetNumberOfComponents();
+  else
+    mode = -1;
+  for (int i = 0; i < data->GetNumberOfCells(); i++)
+  {
+    data->GetCell(i, cell);
+    points = cell->GetPoints();
+
+    // getColors
+    alpha = 1.0;
+    switch (mode)
+    {
+      case -1:
+        actor->GetProperty()->GetColor(color);
+        alpha = actor->GetProperty()->GetOpacity();
+        break;
+      case vtkScalarsToColors::MAGNITUDE:
+        mag = 0;
+        for (int w = 0; w < numberOfComponents; w++)
+          mag += array->GetComponent(i, w) * array->GetComponent(i, w);
+        mag = sqrt(mag);
+        table->GetColor(mag, &color[0]);
+        alpha = table->GetOpacity(mag);
+        break;
+      case vtkScalarsToColors::COMPONENT:
+        mag = array->GetComponent(i, colorComponent);
+        table->GetColor(mag, &color[0]);
+        alpha = table->GetOpacity(mag);
+        break;
+      case vtkScalarsToColors::RGBCOLORS:
+        array->GetTuple(i, &color[0]);
+        break;
+    }
+    // getNormals
+    celldata->GetNormals()->GetTuple(i, &normal[0]);
+    for (int j = 0; j < 3; j++)
+    {
+      aux = i * 9 + j * 3;
+      // Normals
+      normals[aux + 0] = normal[0];
+      normals[aux + 1] = normal[1];
+      normals[aux + 2] = normal[2];
+      // getVertices
+      points->GetPoint(j, &tuple[0]);
+      vertices[aux + 0] = tuple[0];
+      vertices[aux + 1] = tuple[1];
+      vertices[aux + 2] = tuple[2];
+      // Colors
+      colors[4 * (3 * i + j) + 0] = (unsigned char)((int)(color[0] * 255));
+      colors[4 * (3 * i + j) + 1] = (unsigned char)((int)(color[1] * 255));
+      colors[4 * (3 * i + j) + 2] = (unsigned char)((int)(color[2] * 255));
+      colors[4 * (3 * i + j) + 3] = (unsigned char)((int)(alpha * 255));
+      // getIndexes
+      indexes[aux + 0] = aux + 0;
+      indexes[aux + 1] = aux + 1;
+      indexes[aux + 2] = aux + 2;
+    }
+  }
+  object->SetMesh(vertices, data->GetNumberOfCells() * 3, indexes, data->GetNumberOfCells() * 3,
+    normals, colors, nullptr, maxSize);
+  cell->Delete();
+  polynormals->Delete();
+}
+
+void vtkWebGLPolyData::GetColorsFromPointData(
+  unsigned char* color, vtkPointData* pointdata, vtkPolyData* polydata, vtkActor* actor)
+{
+  vtkDataSetAttributes* attr = (vtkDataSetAttributes*)pointdata;
+
+  int colorSize = attr->GetNormals()->GetSize() * 4 / 3;
+
+  vtkDataArray* array;
+  if (actor->GetMapper()->GetArrayAccessMode() == VTK_GET_ARRAY_BY_ID)
+    array = pointdata->GetArray(actor->GetMapper()->GetArrayId());
+  else
+    array = pointdata->GetArray(actor->GetMapper()->GetArrayName());
+
+  if (array && actor->GetMapper()->GetScalarVisibility() &&
+    actor->GetMapper()->GetArrayName() != nullptr && actor->GetMapper()->GetArrayName()[0] != '\0')
+  {
+    vtkScalarsToColors* table = actor->GetMapper()->GetLookupTable();
+    int colorComponent = table->GetVectorComponent(),
+        numberOfComponents = array->GetNumberOfComponents();
+    int mode = table->GetVectorMode();
+    double mag = 0, rgb[3];
+    double alpha = 1.0;
+
+    if (numberOfComponents == 1 && mode == vtkScalarsToColors::MAGNITUDE)
+    {
+      mode = vtkScalarsToColors::COMPONENT;
+      colorComponent = 0;
+    }
+    for (int i = 0; i < colorSize / 4; i++)
+    {
+      switch (mode)
+      {
+        case vtkScalarsToColors::MAGNITUDE:
+          mag = 0;
+          for (int w = 0; w < numberOfComponents; w++)
+            mag += array->GetComponent(i, w) * array->GetComponent(i, w);
+          mag = sqrt(mag);
+          table->GetColor(mag, &rgb[0]);
+          alpha = table->GetOpacity(mag);
+          break;
+        case vtkScalarsToColors::COMPONENT:
+          mag = array->GetComponent(i, colorComponent);
+          table->GetColor(mag, &rgb[0]);
+          alpha = table->GetOpacity(mag);
+          break;
+        case vtkScalarsToColors::RGBCOLORS:
+          array->GetTuple(i, &rgb[0]);
+          alpha = actor->GetProperty()->GetOpacity();
+          break;
+      }
+      color[i * 4 + 0] = (unsigned char)((int)(rgb[0] * 255));
+      color[i * 4 + 1] = (unsigned char)((int)(rgb[1] * 255));
+      color[i * 4 + 2] = (unsigned char)((int)(rgb[2] * 255));
+      color[i * 4 + 3] = (unsigned char)((int)(alpha * 255));
+    }
+  }
+  else
+  {
+    double rgb[3];
+    double alpha = 0;
+    int celldata;
+    array = vtkAbstractMapper::GetScalars(polydata, actor->GetMapper()->GetScalarMode(),
+      actor->GetMapper()->GetArrayAccessMode(), actor->GetMapper()->GetArrayId(),
+      actor->GetMapper()->GetArrayName(), celldata);
+    if (actor->GetMapper()->GetScalarVisibility() &&
+      (actor->GetMapper()->GetColorMode() == VTK_COLOR_MODE_DEFAULT ||
+        actor->GetMapper()->GetColorMode() == VTK_COLOR_MODE_DIRECT_SCALARS) &&
+      array)
+    {
+      vtkScalarsToColors* table = actor->GetMapper()->GetLookupTable();
+      vtkUnsignedCharArray* cor =
+        table->MapScalars(array, actor->GetMapper()->GetColorMode(), table->GetVectorComponent());
+      memcpy(color, cor->GetPointer(0), polydata->GetNumberOfPoints() * 4);
+      cor->Delete();
+    }
+    else
+    {
+      actor->GetProperty()->GetColor(rgb);
+      alpha = actor->GetProperty()->GetOpacity();
+      for (int i = 0; i < colorSize / 4; i++)
+      {
+        color[i * 4 + 0] = (unsigned char)((int)(rgb[0] * 255));
+        color[i * 4 + 1] = (unsigned char)((int)(rgb[1] * 255));
+        color[i * 4 + 2] = (unsigned char)((int)(rgb[2] * 255));
+        color[i * 4 + 3] = (unsigned char)((int)(alpha * 255));
+      }
+    }
+  }
+}
+VTK_ABI_NAMESPACE_END
diff --git a/Web/WebGLExporter/vtkWebGLPolyData.h b/Web/WebGLExporter/vtkWebGLPolyData.h
new file mode 100644 (file)
index 0000000..12e5a3e
--- /dev/null
@@ -0,0 +1,66 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+/**
+ * @class   vtkWebGLPolyData
+ * @brief   PolyData representation for WebGL.
+ */
+
+#ifndef vtkWebGLPolyData_h
+#define vtkWebGLPolyData_h
+
+#include "vtkWebGLExporterModule.h" // needed for export macro
+#include "vtkWebGLObject.h"
+
+VTK_ABI_NAMESPACE_BEGIN
+class vtkActor;
+class vtkMatrix4x4;
+class vtkMapper;
+class vtkPointData;
+class vtkPolyData;
+class vtkTriangleFilter;
+
+class VTKWEBGLEXPORTER_EXPORT vtkWebGLPolyData : public vtkWebGLObject
+{
+public:
+  static vtkWebGLPolyData* New();
+  vtkTypeMacro(vtkWebGLPolyData, vtkWebGLObject);
+  void PrintSelf(ostream& os, vtkIndent indent) override;
+
+  void GenerateBinaryData() override;
+  unsigned char* GetBinaryData(int part) override;
+  int GetBinarySize(int part) override;
+  int GetNumberOfParts() override;
+
+  void GetPoints(vtkTriangleFilter* polydata, vtkActor* actor, int maxSize);
+
+  void GetLinesFromPolygon(vtkMapper* mapper, vtkActor* actor, int lineMaxSize, double* edgeColor);
+  void GetLines(vtkTriangleFilter* polydata, vtkActor* actor, int lineMaxSize);
+  void GetColorsFromPolyData(unsigned char* color, vtkPolyData* polydata, vtkActor* actor);
+
+  // Get following data from the actor
+  void GetPolygonsFromPointData(vtkTriangleFilter* polydata, vtkActor* actor, int maxSize);
+  void GetPolygonsFromCellData(vtkTriangleFilter* polydata, vtkActor* actor, int maxSize);
+  void GetColorsFromPointData(
+    unsigned char* color, vtkPointData* pointdata, vtkPolyData* polydata, vtkActor* actor);
+
+  void SetMesh(float* _vertices, int _numberOfVertices, int* _index, int _numberOfIndexes,
+    float* _normals, unsigned char* _colors, float* _tcoords, int maxSize);
+  void SetLine(float* _points, int _numberOfPoints, int* _index, int _numberOfIndex,
+    unsigned char* _colors, int maxSize);
+  void SetPoints(float* points, int numberOfPoints, unsigned char* colors, int maxSize);
+  void SetTransformationMatrix(vtkMatrix4x4* m);
+
+protected:
+  vtkWebGLPolyData();
+  ~vtkWebGLPolyData() override;
+
+private:
+  vtkWebGLPolyData(const vtkWebGLPolyData&) = delete;
+  void operator=(const vtkWebGLPolyData&) = delete;
+
+  class vtkInternal;
+  vtkInternal* Internal;
+};
+
+VTK_ABI_NAMESPACE_END
+#endif
diff --git a/Web/WebGLExporter/vtkWebGLWidget.cxx b/Web/WebGLExporter/vtkWebGLWidget.cxx
new file mode 100644 (file)
index 0000000..ec2f21c
--- /dev/null
@@ -0,0 +1,157 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include "vtkWebGLWidget.h"
+
+#include "vtkActor2D.h"
+#include "vtkDiscretizableColorTransferFunction.h"
+#include "vtkObjectFactory.h"
+#include "vtkScalarBarActor.h"
+#include "vtkWebGLExporter.h"
+#include "vtkWebGLObject.h"
+
+#include <sstream>
+
+VTK_ABI_NAMESPACE_BEGIN
+vtkStandardNewMacro(vtkWebGLWidget);
+
+vtkWebGLWidget::vtkWebGLWidget()
+{
+  this->binaryData = nullptr;
+  this->iswidget = false;
+  this->binarySize = 0;
+  this->orientation = 1;
+  this->interactAtServer = false;
+  this->title = nullptr;
+}
+
+vtkWebGLWidget::~vtkWebGLWidget()
+{
+  delete[] this->binaryData;
+  while (!this->colors.empty())
+  {
+    double* xrgb = this->colors.back();
+    this->colors.pop_back();
+    delete[] xrgb;
+  }
+  delete[] this->title;
+}
+
+unsigned char* vtkWebGLWidget::GetBinaryData(int vtkNotUsed(part))
+{
+  this->hasChanged = false;
+  return this->binaryData;
+}
+
+int vtkWebGLWidget::GetBinarySize(int vtkNotUsed(part))
+{
+  return this->binarySize;
+}
+
+void vtkWebGLWidget::GenerateBinaryData()
+{
+  delete[] this->binaryData;
+  std::string oldMD5 = this->MD5;
+
+  size_t pos = 0;
+  // Calculate the size used
+  // NumOfColors, Type, Position, Size, Colors, Orientation, numberOfLabels
+  int total = static_cast<int>(sizeof(int) + 1 + 4 * sizeof(float) +
+    this->colors.size() * (sizeof(float) + 3 * sizeof(char)) + 1 + 1 + strlen(this->title));
+  this->binaryData = new unsigned char[total];
+  int colorSize = static_cast<int>(this->colors.size());
+
+  memset(this->binaryData, 0, total); // Fill array with 0
+  memcpy(&this->binaryData[pos], &colorSize, sizeof(int));
+  pos += sizeof(int);            // Binary Data Size
+  this->binaryData[pos++] = 'C'; // Object Type
+  memcpy(&this->binaryData[pos], &this->position, sizeof(float) * 2);
+  pos += sizeof(float) * 2; // Position (double[2])
+  memcpy(&this->binaryData[pos], &this->size, sizeof(float) * 2);
+  pos += sizeof(float) * 2; // Size (double[2])
+  unsigned char rgb[3];
+  for (size_t i = 0; i < colors.size(); i++) // Array of Colors (double, char[3])
+  {
+    float v = (float)this->colors[i][0];
+    memcpy(&this->binaryData[pos], &v, sizeof(float));
+    pos += sizeof(float);
+    rgb[0] = (unsigned char)((int)(this->colors[i][1] * 255));
+    rgb[1] = (unsigned char)((int)(this->colors[i][2] * 255));
+    rgb[2] = (unsigned char)((int)(this->colors[i][3] * 255));
+    memcpy(&this->binaryData[pos], rgb, 3 * sizeof(unsigned char));
+    pos += sizeof(unsigned char) * 3;
+  }
+  unsigned char aux;
+  aux = (unsigned char)this->orientation;
+  memcpy(&this->binaryData[pos], &aux, 1);
+  pos++;
+  aux = (unsigned char)this->numberOfLabels;
+  memcpy(&this->binaryData[pos], &aux, 1);
+  pos++;
+  memcpy(&this->binaryData[pos], this->title, strlen(this->title));
+  pos += strlen(this->title);
+
+  this->binarySize = total;
+  vtkWebGLExporter::ComputeMD5(this->binaryData, total, this->MD5);
+  this->hasChanged = this->MD5 != oldMD5;
+}
+
+int vtkWebGLWidget::GetNumberOfParts()
+{
+  return 1;
+}
+
+void vtkWebGLWidget::PrintSelf(ostream& os, vtkIndent indent)
+{
+  this->Superclass::PrintSelf(os, indent);
+}
+
+void vtkWebGLWidget::GetDataFromColorMap(vtkActor2D* actor)
+{
+  vtkScalarBarActor* scalarbar = vtkScalarBarActor::SafeDownCast(actor);
+  this->numberOfLabels = scalarbar->GetNumberOfLabels();
+
+  std::stringstream theTitle;
+  char* componentTitle = scalarbar->GetComponentTitle();
+
+  theTitle << scalarbar->GetTitle();
+  if (componentTitle && strlen(componentTitle) > 0)
+  {
+    theTitle << " ";
+    theTitle << componentTitle;
+  }
+
+  delete[] this->title;
+  std::string tmp = theTitle.str();
+  this->title = new char[tmp.length() + 1];
+  strcpy(this->title, tmp.c_str());
+  this->hasTransparency = (scalarbar->GetUseOpacity() != 0);
+  this->orientation = scalarbar->GetOrientation();
+
+  // Colors
+  vtkDiscretizableColorTransferFunction* lookup =
+    vtkDiscretizableColorTransferFunction::SafeDownCast(scalarbar->GetLookupTable());
+  int num = 5 * lookup->GetSize();
+  double* range = lookup->GetRange();
+  double v, s;
+  v = range[0];
+  s = (range[1] - range[0]) / (num - 1);
+  for (int i = 0; i < num; i++)
+  {
+    double* xrgb = new double[4];
+    scalarbar->GetLookupTable()->GetColor(v, &xrgb[1]);
+    xrgb[0] = v;
+    this->colors.push_back(xrgb);
+    v += s;
+  }
+
+  this->textFormat = scalarbar->GetLabelFormat();    // Float Format ex.: %-#6.3g
+  this->textPosition = scalarbar->GetTextPosition(); // Orientacao dos textos; 1;
+  double* thePos = scalarbar->GetPosition();
+  double* theSize = scalarbar->GetPosition2();
+  this->position[0] = thePos[0];
+  this->position[1] = thePos[1]; // Widget Position
+  this->size[0] = theSize[0];
+  this->size[1] = theSize[1]; // Widget Size
+}
+VTK_ABI_NAMESPACE_END
diff --git a/Web/WebGLExporter/vtkWebGLWidget.h b/Web/WebGLExporter/vtkWebGLWidget.h
new file mode 100644 (file)
index 0000000..90b9c9c
--- /dev/null
@@ -0,0 +1,54 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+/**
+ * @class   vtkWebGLWidget
+ * @brief   Widget representation for WebGL.
+ */
+
+#ifndef vtkWebGLWidget_h
+#define vtkWebGLWidget_h
+
+#include "vtkWebGLExporterModule.h" // needed for export macro
+#include "vtkWebGLObject.h"
+
+#include <vector> // Needed to store colors
+
+VTK_ABI_NAMESPACE_BEGIN
+class vtkActor2D;
+
+class VTKWEBGLEXPORTER_EXPORT vtkWebGLWidget : public vtkWebGLObject
+{
+public:
+  static vtkWebGLWidget* New();
+  vtkTypeMacro(vtkWebGLWidget, vtkWebGLObject);
+  void PrintSelf(ostream& os, vtkIndent indent) override;
+
+  void GenerateBinaryData() override;
+  unsigned char* GetBinaryData(int part) override;
+  int GetBinarySize(int part) override;
+  int GetNumberOfParts() override;
+
+  void GetDataFromColorMap(vtkActor2D* actor);
+
+protected:
+  vtkWebGLWidget();
+  ~vtkWebGLWidget() override;
+
+  unsigned char* binaryData;
+  int binarySize;
+  int orientation;
+  char* title;
+  char* textFormat;
+  int textPosition;
+  float position[2];
+  float size[2];
+  int numberOfLabels;
+  std::vector<double*> colors; // x, r, g, b
+
+private:
+  vtkWebGLWidget(const vtkWebGLWidget&) = delete;
+  void operator=(const vtkWebGLWidget&) = delete;
+};
+
+VTK_ABI_NAMESPACE_END
+#endif
diff --git a/Web/WebGLExporter/webglRenderer.js b/Web/WebGLExporter/webglRenderer.js
new file mode 100644 (file)
index 0000000..e0e1644
--- /dev/null
@@ -0,0 +1,1307 @@
+/**
+ * Create a renderer object working fully in WebGL
+ * Here is a sample set of command to illustrate how to use this renderer
+ *
+ * var renderer = new WebGLRenderer('rendererId','http://localhost:8080/ParaViewWebService')
+ * renderer.init(sessionId, viewId);
+ * renderer.bindToElementId('containerID'); // => Add a WebGL canvas inside a div tag id 'containerID'
+ * renderer.start();
+ *
+ * renderer.init(otherSessionId, otherViewId);
+ * renderer.view.width = '100';
+ * renderer.view.height = '400';
+ * renderer.setSize('100', '400');
+ *
+ * renderer.unbindToElementId('containerID');
+ */
+
+// Global object to keep track of WebGL renderers
+var webglRenderers = new Object();
+
+window.requestAnimFrame = (function(){
+  return  window.requestAnimationFrame       ||
+          window.webkitRequestAnimationFrame ||
+          window.mozRequestAnimationFrame    ||
+          window.oRequestAnimationFrame      ||
+          window.msRequestAnimationFrame     ||
+          function(/* function */ callback, /* DOMElement */ element){
+            window.setTimeout(callback, 1000 / 60);
+          };
+})();
+
+function WebGLRenderer(rendererId, coreServiceURL) {
+    this.baseURL = coreServiceURL + "/WebGL";
+    this.rendererId = rendererId;
+    this.sessionId = "";
+    this.viewId = "";
+    this.nbError = 0;
+    this.localTimeStamp = 0;
+    this.offlineMode = false;
+    this.setServerMode(false);
+    this.forceSquareSize = false;
+
+    this.view = new Object();
+    this.view.width = 100;
+    this.view.height = 100;
+    this.view.id = rendererId;
+    this.view.alt = "ParaView Renderer";
+
+    //Default Shaders
+    this.view.shaderfs = document.createElement("script");
+    this.view.shaderfs.id = "shader-fs";
+    this.view.shaderfs.type = "x-shader/x-fragment";
+    this.view.shaderfs.innerHTML = "\
+    #ifdef GL_ES\n\
+    precision highp float;\n\
+    #endif\n\
+    uniform bool uIsLine;\
+    varying vec4 vColor;\
+    varying vec4 vTransformedNormal;\
+    varying vec4 vPosition;\
+    void main(void) {\
+        float directionalLightWeighting1 = max(dot(normalize(vTransformedNormal.xyz), vec3(0.0, 0.0, 1.0)), 0.0); \
+        float directionalLightWeighting2 = max(dot(normalize(vTransformedNormal.xyz), vec3(0.0, 0.0, -1.0)), 0.0);\
+        vec3 lightWeighting = max(vec3(1.0, 1.0, 1.0) * directionalLightWeighting1, vec3(1.0, 1.0, 1.0) * directionalLightWeighting2);\
+        if (uIsLine == false){\
+          gl_FragColor = vec4(vColor.rgb * lightWeighting, vColor.a);\
+        } else {\
+          gl_FragColor = vColor*vec4(1.0, 1.0, 1.0, 1.0);\
+        }\
+    }";
+    this.view.shadervs = document.createElement("script");
+    this.view.shadervs.id = "shader-vs";
+    this.view.shadervs.type = "x-shader/x-vertex";
+    this.view.shadervs.innerHTML = "\
+    attribute vec3 aVertexPosition;\
+    attribute vec4 aVertexColor;\
+    attribute vec3 aVertexNormal;\
+    uniform mat4 uMVMatrix;\
+    uniform mat4 uPMatrix;\
+    uniform mat4 uNMatrix;\
+    varying vec4 vColor;\
+    varying vec4 vPosition;\
+    varying vec4 vTransformedNormal;\
+    void main(void) {\
+        vPosition = uMVMatrix * vec4(aVertexPosition, 1.0);\
+        gl_Position = uPMatrix * vPosition;\
+        vTransformedNormal = uNMatrix * vec4(aVertexNormal, 1.0);\
+        vColor = aVertexColor;\
+    }";
+
+    // Point Shaders
+    this.view.shaderfsPoint = document.createElement("script");
+    this.view.shaderfsPoint.id = "shader-fs-Point";
+    this.view.shaderfsPoint.type = "x-shader/x-fragment";
+    this.view.shaderfsPoint.innerHTML = "\
+    #ifdef GL_ES\n\
+    precision highp float;\n\
+    #endif\n\
+    varying vec4 vColor;\
+    void main(void) {\
+        gl_FragColor = vColor;\
+    }";
+    this.view.shadervsPoint = document.createElement("script");
+    this.view.shadervsPoint.id = "shader-vs-Point";
+    this.view.shadervsPoint.type = "x-shader/x-vertex";
+    this.view.shadervsPoint.innerHTML = "\
+    attribute vec3 aVertexPosition;\
+    attribute vec4 aVertexColor;\
+    uniform mat4 uMVMatrix;\
+    uniform mat4 uPMatrix;\
+    uniform mat4 uNMatrix;\
+    uniform float uPointSize;\
+    varying vec4 vColor;\
+    void main(void) {\
+        vec4 pos = uMVMatrix * vec4(aVertexPosition, 1.0);\
+        gl_Position = uPMatrix * pos;\
+        vColor = aVertexColor*vec4(1.0, 1.0, 1.0, 1.0);\
+        gl_PointSize = uPointSize;\
+    }";
+
+    //
+    this.canvasName = "glcanvas" + rendererId;
+    this.view.html = '<div><canvas id="' + this.canvasName + '" style="border: none; overflow: hidden;';
+    if (this.forceSquareSize == true) this.view.html += ' position: absolute;';
+    this.view.html += ' left:-1px; top:-1px; right:0px; z-index=0;" width="' + this.view.width
+    + '" height="' + this.view.height + '"onmousedown="handleMouseDown(event,\''+rendererId+'\')"\
+     onmousemove="handleMouseMove(event,\''+rendererId+'\')" onmouseup="handleMouseUp(event,\'' + rendererId
+     + '\')" oncontextmenu="consumeEvent(event)"> Your browser doesn\'t appear to support the HTML5 <code>\
+     &lt;canvas&gt;</code> element.</canvas>';
+
+     this.view.html += '<canvas id="' + this.canvasName + 'Widget" style="position: absolute; left:-1px; top:-1px; z-index:1;';
+     if(this.forceSquareSize == true) this.view.html += 'position: absolute;';
+     this.view.html += '" width="' + this.view.width + '" height="' + this.view.height +
+     '"onmousedown="handleMouseDown(event,\''+rendererId+'\')" onmousemove="handleMouseMove(event,\''+rendererId+'\')"\
+     onmouseup="handleMouseUp(event,\'' + rendererId + '\')" oncontextmenu="consumeEvent(event)"></canvas></div>';
+    this.fps = 0;
+
+    // Register in global var
+    webglRenderers[rendererId] = this;
+}
+
+WebGLRenderer.prototype.bindToElementId = function (elementId) {
+    this.oldInnerHTML = document.getElementById(elementId).innerHTML;
+    document.getElementById(elementId).innerHTML = this.view.html;
+
+    document.getElementById(elementId).appendChild(this.view.shaderfs);
+    document.getElementById(elementId).appendChild(this.view.shadervs);
+    document.getElementById(elementId).appendChild(this.view.shaderfsPoint);
+    document.getElementById(elementId).appendChild(this.view.shadervsPoint);
+}
+
+WebGLRenderer.prototype.unbindToElementId = function (elementId) {
+  document.getElementById(elementId).innerHTML = this.oldInnerHTML;
+  clearTimeout(this.drawInterval);
+  if (typeof(paraview) != "undefined") paraview.updateConfiguration(true, "JPEG", "NO");
+}
+
+WebGLRenderer.prototype.setOfflineMode = function (mode) {
+  this.offlineMode = mode;
+  this.requestMetaData();
+}
+
+WebGLRenderer.prototype.bindToElement = function (element) {
+    this.oldInnerHTML = element.innerHTML;
+    element.innerHTML = this.view.html;
+
+    element.appendChild(this.view.shaderfs);
+    element.appendChild(this.view.shadervs);
+    element.appendChild(this.view.shaderfsPoint);
+    element.appendChild(this.view.shadervsPoint);
+}
+
+WebGLRenderer.prototype.unbindToElement = function (element) {
+  element.innerHTML = this.oldInnerHTML;
+  clearTimeout(this.drawInterval);
+  if (typeof(paraview) != "undefined") paraview.updateConfiguration(true, "JPEG", "NO");
+}
+
+WebGLRenderer.prototype.init = function (sessionId, viewId) {
+    this.sessionId = sessionId;
+    this.viewId = viewId;
+}
+
+WebGLRenderer.prototype.start = function(metadata, objects) {
+    if (typeof(renderers) == "undefined"){
+      renderers = Object();
+      renderers.current = this;
+    }
+    if (typeof(paraview) != "undefined") paraview.updateConfiguration(true, "JPEG", "WebGL");
+    canvas = document.getElementById(this.canvasName);
+    canvas.width = this.view.width;
+    canvas.height = this.view.height;
+
+    this.hasSceneChanged = true;        //Scene Graph Has Changed
+    this.oldCamPos = null;              //Last Known Camera Position
+    this.sceneJSON = null;              //Current Scene Graph
+    this.up = [];
+    this.right = [];
+    this.z_dir = [];
+    this.objects = [];                  //List of objects
+    this.nbErrors = 0;                  //Number of Errors
+    this.background = null;             //Background object: mesh, normals, colors, render
+    this.interactionRatio = 2;
+    this.requestInterval = 250;         //Frequency it request new data from the server
+    this.requestOldInterval = 250;      //
+    this.updateInterval = 100;          //Frequency the server will be updated
+    this.fps = 0;
+    this.frames = 0;
+    this.lastTime = new Date().getTime();
+    this.view.aspectRatio = 1;
+    this.lookAt = [0,0,0,0,1,0,0,0,1];
+    this.offlineMode = !(typeof(metadata)=="undefined" || typeof(objects)=="undefined");
+
+    this.cachedObjects = [];            //List of Cached Objects
+    this.isCaching = false;             //Is Caching or Not
+
+    this.processQueue = [];             //List of process to be executed
+
+    this.objScale = 1.0;                //Scale applied locally in the scene
+    this.translation = [0.0, 0.0, 0.0]; //Translation
+    this.rotMatrix = mat4.create();     //Rotation Matrix
+    mat4.identity(this.rotMatrix);
+    this.rotMatrix2 = mat4.create(this.rotMatrix);
+
+    this.mouseDown = false;
+    this.lastMouseX = 0;
+    this.lastMouseY = 0;
+
+    this.mvMatrix = mat4.create(this.rotMatrix);
+    this.pMatrix = mat4.create(this.rotMatrix);
+
+    // Initialize the GL context
+    this.gl = null;
+    try {
+      this.gl = canvas.getContext("experimental-webgl");
+      this.gl.viewportWidth = this.view.width;
+      this.gl.viewportHeight = this.view.height;
+    } catch(e) {}
+
+    if (this.gl) {
+      this.gl.clearColor(0.0, 0.0, 0.0, 1.0);
+      this.gl.clearDepth(1.0);
+      this.gl.enable(this.gl.DEPTH_TEST);
+      this.gl.depthFunc(this.gl.LEQUAL);
+
+      this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
+
+      this.initShaders();
+
+      this.ctx2d = document.getElementById(this.canvasName + "Widget").getContext('2d');
+      // Set up to draw the scene periodically.
+      this.drawInterval = requestAnimFrame(new Function("webglRenderers['" + this.view.id + "'].drawScene();"));
+
+      if (!this.offlineMode){
+        this.requestMetaData();
+        this.updateCamera();
+      } else {
+        this.sceneJSON = JSON.parse(metadata);
+
+        for(aw=0; aw<objects.length-1; aw++){
+          obj = new Object();
+          obj.data = objects[aw];
+          obj.hasTransparency = this.sceneJSON.Objects[aw].transparency;
+          obj.layer = this.sceneJSON.Objects[aw].layer;
+          obj.render = function(){};
+          this.processQueue[aw] = obj;
+          this.objects[this.objects.length] = obj;
+        }
+      }
+    } else {
+      canvas.parentNode.innerHTML = "<table width=100% height=100%><tr><td align=center>\
+      Sorry, your browser do not support WebGL.<br> For more information visit the website\
+      <a href='http://get.webgl.org/' target='_blank'>http://get.webgl.org/</a></td></tr></table>";
+    }
+}
+
+WebGLRenderer.prototype.setForceSquareSize = function(b){
+this.forceSquareSize = b;
+}
+
+WebGLRenderer.prototype.getPageX = function(){
+    var location = 0;
+    var node = document.getElementById(this.canvasName);
+    while(node) {
+        location += node.offsetLeft;
+        node = node.offsetParent;
+    }
+    return location;
+}
+
+WebGLRenderer.prototype.getPageY = function(){
+    var location = 0;
+    var node = document.getElementById(this.canvasName);
+    while(node) {
+        location += node.offsetTop;
+        node = node.offsetParent;
+    }
+    return location;
+}
+
+WebGLRenderer.prototype.setServerMode = function(mode){
+  if (typeof(this.interaction) == "undefined"){
+    this.interaction = new Object();
+    this.interaction.lastRealEvent = 0;
+    this.interaction.needUp = false;
+    this.interaction.lastEvent = 0;
+    this.interaction.isDragging = false;
+    this.interaction.action = " ";
+    this.interaction.keys = " ";
+    this.interaction.button = 0;
+    this.interaction.x = 0;
+    this.interaction.y = 0;
+    this.interaction.xOrigin = 0;
+    this.interaction.yOrigin = 0;
+    this.serverMode = false;
+  }
+  if (this.serverMode == mode) return;
+  this.serverMode = mode;
+  if (!this.serverMode){
+    this.updateId = setTimeout("webglRenderers[\'" + this.view.id + "\'].updateCamera()", this.updateInterval);
+  }
+  canvas = document.getElementById(this.canvasName);
+  canvasWidget = document.getElementById(this.canvasName + "Widget");
+  if (this.serverMode){
+    this.requestOldInterval = this.requestInterval;
+    this.requestInterval = 50;
+    canvas.setAttribute("onmousedown", "mouseServerInt('"+this.view.id+"','"+this.sessionId+"','"+this.viewId+"','down',event)");
+    canvas.setAttribute("onmousemove", "mouseServerInt('"+this.view.id+"','"+this.sessionId+"','"+this.viewId+"','move',event)");
+    canvas.setAttribute("onmouseup"  , "mouseServerInt('"+this.view.id+"','"+this.sessionId+"','"+this.viewId+"','up',event)");
+    canvasWidget.setAttribute("onmousedown", "mouseServerInt('"+this.view.id+"','"+this.sessionId+"','"+this.viewId+"','down',event)");
+    canvasWidget.setAttribute("onmousemove", "mouseServerInt('"+this.view.id+"','"+this.sessionId+"','"+this.viewId+"','move',event)");
+    canvasWidget.setAttribute("onmouseup"  , "mouseServerInt('"+this.view.id+"','"+this.sessionId+"','"+this.viewId+"','up',event)");
+  } else {
+    this.requestInterval = this.requestOldInterval;
+    canvas.setAttribute("onmousedown", "handleMouseDown(event,'" + this.rendererId + "')");
+    canvas.setAttribute("onmousemove", "handleMouseMove(event,'" + this.rendererId + "')");
+    canvas.setAttribute("onmouseup"  , "handleMouseUp(event,'" + this.rendererId + "')");
+    canvasWidget.setAttribute("onmousedown", "handleMouseDown(event,'" + this.rendererId + "')");
+    canvasWidget.setAttribute("onmousemove", "handleMouseMove(event,'" + this.rendererId + "')");
+    canvasWidget.setAttribute("onmouseup"  , "handleMouseUp(event,'" + this.rendererId + "')");
+  }
+  canvas.setAttribute("oncontextmenu", "consumeEvent(event)");
+}
+
+WebGLRenderer.prototype.setSize = function(width, height) {
+    width = parseFloat(width);
+    height = parseFloat(height);
+    w = width;
+    h = height;
+    this.view.aspectRatio = width/height;
+    if(this.forceSquareSize){
+      if (width > height) height = width;
+      else width = height;
+    }
+    this.view.width = width;
+    this.view.height = height;
+    canvas = document.getElementById(this.canvasName);
+    canvasWidget = document.getElementById(this.canvasName + "Widget");
+    if (canvas){
+      canvas.width = this.view.width;
+      canvas.height = this.view.height;
+      canvasWidget.width = this.view.width;
+      canvasWidget.height = this.view.height;
+      if (typeof(this.gl) != "undefined" && this.gl != null){
+        if (!this.offlineMode) updateRendererSize(this.sessionId, this.viewId, width, height);
+          this.gl.viewportWidth = this.view.width;
+          this.gl.viewportHeight = this.view.height;
+        }
+        left = 0; tt = 0;
+        if (this.forceSquareSize){
+          left = Math.round((w-this.view.width)/2);
+          tt = Math.round((h-this.view.height)/2);
+        }
+        this.view.left = left;
+        this.view.top = top;
+        if(this.forceSquareSize == true){
+          canvas.setAttribute("style", "position: absolute; overflow: hidden; left: " + left + "px; top: " + tt + "px; right: 0px; z-index:0;");
+          canvasWidget.setAttribute("style", "position: absolute; overflow: hidden; left: " + left + "px; top: " + tt + "px; right: 0px; z-index:1;");
+        } else {
+          canvas.setAttribute("style", "overflow: hidden; left: " + left + "px; top: " + tt + "px; right: 0px; z-index:0;");
+          canvasWidget.setAttribute("style", "position: absolute; overflow: hidden; left: " + left + "px; top: " + tt + "px; right: 0px; z-index:1;");
+        }
+    }
+}
+
+WebGLRenderer.prototype.requestMetaData = function() {
+  if (this.mouseDown || renderers.current != this) return;
+  if (this.offlineMode) return;
+
+  interval = this.requestInterval;
+  if (this.serverMode) interval = interval/2;
+  this.timer = setTimeout("webglRenderers[\'" + this.view.id + "\'].requestMetaData()", interval);
+  var request = new XMLHttpRequest();
+  request.requester = this;
+  filename = this.baseURL + "?sid=" + this.sessionId + "&vid=" + this.viewId + "&q=meta";
+  try {
+    request.open("GET", filename, false);
+    request.overrideMimeType('text/plain; charset=x-user-defined');
+    request.onreadystatechange = function() {
+      if(this.requester.mouseDown) return;
+      if (request.status != 200) this.requester.nbErrors++
+      else if (request.readyState == 4) {
+        aux = JSON.parse(request.responseText);
+        this.requester.hasSceneChanged = JSON.stringify(aux)!=JSON.stringify(this.requester.sceneJSON);
+        this.requester.sceneJSON = JSON.parse(request.responseText);
+        if (this.requester.hasSceneChanged) this.requester.updateScene();
+      }
+    }
+  request.send();
+  } catch (e) {
+    this.nbErrors++;
+  }
+}
+
+WebGLRenderer.prototype.updateScene = function(){
+  if (typeof(this.sceneJSON) == "undefined" || this.sceneJSON == null) return;
+  c1 = [0,0,0];
+  c2 = [0,0,0];
+  for(l=0; l<this.sceneJSON.Renderers.length; l++){
+    if(this.sceneJSON.Renderers[l].layer==0){
+      this.lookAt = this.sceneJSON.Renderers[l].LookAt;
+      c1 = this.sceneJSON.Renderers[l].Background1;
+      if (typeof(this.sceneJSON.Renderers[l].Background2) != "undefined") c2 = this.sceneJSON.Renderers[l].Background2;
+    }
+  }
+  this.initBackground(c1, c2);
+  if (JSON.stringify(this.oldCamPos)!=JSON.stringify(this.lookAt)){
+    this.translation = [0.0, 0.0, 0.0];
+    this.objScale = 1.0;
+    mat4.identity(this.rotMatrix);
+
+    this.up = [this.lookAt[4], this.lookAt[5], this.lookAt[6]];
+    this.z_dir = [this.lookAt[1]-this.lookAt[7],
+                  this.lookAt[2]-this.lookAt[8],
+                  this.lookAt[3]-this.lookAt[9]];
+    vec3.normalize(this.z_dir, this.z_dir);
+    vec3.cross(this.z_dir, this.up, this.right);
+  }
+  this.oldCamPos = this.lookAt;
+  var aux = [];
+  intAtServer = false;
+  if (!this.offlineMode){
+    for(w=0; w<this.objects.length; w++){
+      for(j=0; j<this.sceneJSON.Objects.length; j++){
+        if (this.objects[w].md5 == this.sceneJSON.Objects[j].md5 && this.objects[w].id == this.sceneJSON.Objects[j].id){
+          aux[aux.length] = this.objects[w];
+        }
+      }
+    }
+    this.objects = aux;
+
+    for(w=0; w<this.sceneJSON.Objects.length; w++){
+      foundit = false;
+
+      if (this.isCaching){
+        for(j=0; j<this.cachedObjects.length; j++)
+          if (this.cachedObjects[j].md5==this.sceneJSON.Objects[w].md5 &&
+              this.cachedObjects[j].id==this.sceneJSON.Objects[w].id){
+            this.objects[this.objects.length] = this.cachedObjects[j];
+            foundit = true;
+          }
+      }
+      if (!foundit){
+        for(k=0; k<this.sceneJSON.Objects[w].parts; k++){
+          foundit = false;
+          for(j=0; j<this.objects.length; j++){
+            if (this.objects[j].md5==this.sceneJSON.Objects[w].md5 &&
+              this.objects[j].id==this.sceneJSON.Objects[w].id && this.objects[j].part==k+1 )
+              foundit=true;
+            }
+            if(!foundit) this.requestObject(this.sessionId, this.sceneJSON.id, this.sceneJSON.Objects[w].md5,
+                                      k+1, this.sceneJSON.Objects[w].id, this.sceneJSON.Objects[w].transparency, this.sceneJSON.Objects[w].layer);
+          }
+      }
+      if (this.sceneJSON.Objects[w].interactAtServer==1) intAtServer = true;
+    }
+  }
+  this.hasSceneChanged = false;
+  this.setServerMode(intAtServer);
+}
+
+WebGLRenderer.prototype.requestObject = function(sid, vid, md5, part, id, hastransparency, layer){
+  if (this.offlineMode) return;
+  var request = new XMLHttpRequest();
+  request.requester = this;
+  filename = this.baseURL + "?sid=" + sid + "&vid=" + vid + "&hash=" + md5 + "&part=" + part + "&q=mesh&id=" + id;
+  try {
+    request.open("GET", filename, false);
+    request.overrideMimeType('text/plain; charset=x-user-defined');
+    request.onreadystatechange = function() {
+      if (request.status != 200) this.requester.nbErrors++
+      else if (request.readyState == 4) {
+        foundit = -1;
+        for (i=0; i<this.requester.objects.length; i++)
+          if (this.requester.objects[i].md5 == md5 && this.requester.objects[i].part == part
+              && this.requester.objects[i].id == id) foundit = i;
+        if (foundit == -1){
+          foundit = this.requester.objects.length;
+          this.requester.objects.length++;
+        }
+        this.requester.objects[foundit] = new Object();
+        this.requester.objects[foundit].md5 = md5;    //hash
+        this.requester.objects[foundit].part = part;  //part
+        this.requester.objects[foundit].sid = sid;    //scene id
+        this.requester.objects[foundit].vid = vid;    //view id
+        this.requester.objects[foundit].id = id;      //object id
+        this.requester.objects[foundit].data = request.responseText;
+        this.requester.objects[foundit].hasTransparency = hastransparency;
+        this.requester.objects[foundit].layer = layer;
+        this.requester.objects[foundit].render = function(){};
+        this.requester.processQueue[this.requester.processQueue.length] = this.requester.objects[foundit];
+        this.requester.cachedObjects[this.requester.cachedObjects.length] = this.requester.objects[foundit];
+      }
+    }
+    request.send();
+  } catch (e){
+    this.nbErrors++;
+  }
+}
+
+WebGLRenderer.prototype.parseObject = function(obj){
+  var ss = []; pos = 0;
+  for(i=0; i<obj.data.length; i++) ss[i] = obj.data.charCodeAt(i) & 0xff;
+
+  size = (ss[pos++]) + (ss[pos++] << 8) + (ss[pos++] << 16) + (ss[pos++] << 24);
+  type = String.fromCharCode(ss[pos++]);
+  obj.type = type;
+  obj.father = this;
+
+  if (type == 'L'){
+    obj.numberOfPoints = (ss[pos++]) + (ss[pos++] << 8) + (ss[pos++] << 16) + (ss[pos++] << 24);
+    //Getting Points
+    test = new Int8Array(obj.numberOfPoints*4*3); for(i=0; i<obj.numberOfPoints*4*3; i++) test[i] = ss[pos++];
+    obj.points = new Float32Array(test.buffer);
+    //Generating Normals
+    test = new Array(obj.numberOfPoints*3); for(i=0; i<obj.numberOfPoints*3; i++) test[i] = 0.0;
+    obj.normals = new Float32Array(test);
+    //Getting Colors
+    test = []; for(i=0; i<obj.numberOfPoints*4; i++) test[i] = ss[pos++]/255.0;
+    obj.colors = new Float32Array(test);
+
+    obj.numberOfIndex = (ss[pos++]) + (ss[pos++] << 8) + (ss[pos++] << 16) + (ss[pos++] << 24);
+    //Getting Index
+    test = new Int8Array(obj.numberOfIndex*2); for(i=0; i<obj.numberOfIndex*2; i++) test[i] = ss[pos++];
+    obj.index = new Uint16Array(test.buffer);
+    //Getting Matrix
+    test = new Int8Array(16*4); for(i=0; i<16*4; i++) test[i] = ss[pos++];
+    obj.matrix = new Float32Array(test.buffer);
+
+    //Creating Buffers
+    obj.lbuff = this.gl.createBuffer(); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, obj.lbuff);
+    this.gl.bufferData(this.gl.ARRAY_BUFFER, obj.points, this.gl.STATIC_DRAW); obj.lbuff.itemSize = 3;
+
+    obj.nbuff = this.gl.createBuffer(); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, obj.nbuff);
+    this.gl.bufferData(this.gl.ARRAY_BUFFER, obj.normals, this.gl.STATIC_DRAW);  obj.nbuff.itemSize = 3;
+
+    obj.cbuff = this.gl.createBuffer(); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, obj.cbuff);
+    this.gl.bufferData(this.gl.ARRAY_BUFFER, obj.colors, this.gl.STATIC_DRAW);   obj.cbuff.itemSize = 4;
+
+    obj.ibuff = this.gl.createBuffer(); this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, obj.ibuff);
+    this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, obj.index, this.gl.STREAM_DRAW);
+
+    obj.render = this.renderLine;
+  }
+
+  //-=-=-=-=-=[ MESH ]=-=-=-=-=-
+  else if (type == 'M'){
+    obj.numberOfVertices = (ss[pos++]) + (ss[pos++] << 8) + (ss[pos++] << 16) + (ss[pos++] << 24);
+    //Getting Vertices
+    test = new Int8Array(obj.numberOfVertices*4*3); for(i=0; i<obj.numberOfVertices*4*3; i++) test[i] = ss[pos++];
+    obj.vertices = new Float32Array(test.buffer);
+    //Getting Normals
+    test = new Int8Array(obj.numberOfVertices*4*3); for(i=0; i<obj.numberOfVertices*4*3; i++) test[i] = ss[pos++];
+    obj.normals = new Float32Array(test.buffer);
+    //Getting Colors
+    test = []; for(i=0; i<obj.numberOfVertices*4; i++) test[i] = ss[pos++]/255.0;
+    obj.colors = new Float32Array(test);
+
+    obj.numberOfIndex = (ss[pos++]) + (ss[pos++] << 8) + (ss[pos++] << 16) + (ss[pos++] << 24);
+    //Getting Index
+    test = new Int8Array(obj.numberOfIndex*2); for(i=0; i<obj.numberOfIndex*2; i++) test[i] = ss[pos++];
+    obj.index = new Uint16Array(test.buffer);
+    //Getting Matrix
+    test = new Int8Array(16*4); for(i=0; i<16*4; i++) test[i] = ss[pos++];
+    obj.matrix = new Float32Array(test.buffer);
+    //Getting TCoord
+    obj.tcoord = null;
+
+    //Create Buffers
+    obj.vbuff = this.gl.createBuffer(); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, obj.vbuff);
+    this.gl.bufferData(this.gl.ARRAY_BUFFER, obj.vertices, this.gl.STATIC_DRAW); obj.vbuff.itemSize = 3;
+
+    obj.nbuff = this.gl.createBuffer(); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, obj.nbuff);
+    this.gl.bufferData(this.gl.ARRAY_BUFFER, obj.normals, this.gl.STATIC_DRAW);  obj.nbuff.itemSize = 3;
+
+    obj.cbuff = this.gl.createBuffer(); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, obj.cbuff);
+    this.gl.bufferData(this.gl.ARRAY_BUFFER, obj.colors, this.gl.STATIC_DRAW);   obj.cbuff.itemSize = 4;
+
+    obj.ibuff = this.gl.createBuffer(); this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, obj.ibuff);
+    this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, obj.index, this.gl.STREAM_DRAW);
+
+    obj.render = this.renderMesh;
+  }
+
+  // ColorMap Widget
+  else if (type == 'C'){
+    obj.numOfColors = size;
+
+    //Getting Position
+    test = new Int8Array(2*4); for(i=0; i<2*4; i++) test[i] = ss[pos++];
+    obj.position = new Float32Array(test.buffer);
+
+    //Getting Size
+    test = new Int8Array(2*4); for(i=0; i<2*4; i++) test[i] = ss[pos++];
+    obj.size = new Float32Array(test.buffer);
+
+    //Getting Colors
+    obj.colors = [];
+    for(c=0; c<obj.numOfColors; c++){
+      test = new Int8Array(4); for(i=0; i<4; i++) test[i] = ss[pos++];
+      v = new Float32Array(test.buffer);
+      xrgb = [v[0], ss[pos++], ss[pos++], ss[pos++]];
+      obj.colors[c] = xrgb;
+    }
+
+    obj.orientation = ss[pos++];
+    obj.numOfLabels = ss[pos++];
+    tt = "";
+    for(jj=0; jj<(ss.length-pos); jj++) tt = tt + String.fromCharCode(ss[pos+jj]);
+    obj.title = tt;
+
+    obj.render = this.renderColorMap;
+  }
+
+  // Points
+  else if (type == 'P'){
+    obj.numberOfPoints = (ss[pos++]) + (ss[pos++] << 8) + (ss[pos++] << 16) + (ss[pos++] << 24);
+    //Getting Points
+    test = new Int8Array(obj.numberOfPoints*4*3); for(i=0; i<obj.numberOfPoints*4*3; i++) test[i] = ss[pos++];
+    obj.points = new Float32Array(test.buffer);
+
+    //Getting Colors
+    test = []; for(i=0; i<obj.numberOfPoints*4; i++) test[i] = ss[pos++]/255.0;
+    obj.colors = new Float32Array(test);
+
+    //Getting Matrix //Wendel
+    test = new Int8Array(16*4); for(i=0; i<16*4; i++) test[i] = ss[pos++];
+    obj.matrix = new Float32Array(test.buffer);
+
+    //Creating Buffers
+    obj.pbuff = this.gl.createBuffer(); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, obj.pbuff);
+    this.gl.bufferData(this.gl.ARRAY_BUFFER, obj.points, this.gl.STATIC_DRAW); obj.pbuff.itemSize = 3;
+
+    obj.cbuff = this.gl.createBuffer(); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, obj.cbuff);
+    this.gl.bufferData(this.gl.ARRAY_BUFFER, obj.colors, this.gl.STATIC_DRAW);   obj.cbuff.itemSize = 4;
+
+    obj.render = this.renderPoints;
+  }
+}
+
+WebGLRenderer.prototype.renderColorMap = function(){
+  obj = this;
+  render = this.father;
+
+  range = [obj.colors[0][0], obj.colors[obj.colors.length-1][0]];
+  size = [obj.size[0]*render.view.width, obj.size[1]*render.view.height];
+  pos = [obj.position[0]*render.view.width, (1-obj.position[1])*render.view.height];
+  pos[1] = pos[1]-size[1];
+  dx = size[0]/size[1];
+  dy = size[1]/size[0];
+  realSize = size;
+
+  textSizeX = Math.round(render.view.height/35);
+  textSizeY = Math.round(render.view.height/23);
+  if (obj.orientation == 1){
+    size[0] = size[0]*dy/25;
+    size[1] = size[1]-(2*textSizeY);
+  } else {
+    size[0] = size[0];
+    size[1] = size[1]*dx/25;
+  }
+
+  // Draw Gradient
+  ctx = this.father.ctx2d;
+  if(obj.orientation == 1){
+    pos[1] += 2*textSizeY;
+    grad = ctx.createLinearGradient(pos[0], pos[1], pos[0], pos[1]+size[1]);
+  } else {
+    pos[1] += 2*textSizeY;
+    grad = ctx.createLinearGradient(pos[0], pos[1], pos[0]+size[0], pos[1]);
+  }
+  if ((range[1]-range[0]) == 0){
+      color = 'rgba(' + obj.colors[0][1] + ',' + obj.colors[0][2] + ',' + obj.colors[0][3] + ',1)';
+      grad.addColorStop(0, color);
+      grad.addColorStop(1, color);
+  } else {
+    for(c=0; c<obj.colors.length; c++){
+      v = ((obj.colors[c][0]-range[0])/(range[1]-range[0]));
+      if (obj.orientation == 1) v=1-v;
+      color = 'rgba(' + obj.colors[c][1] + ',' + obj.colors[c][2] + ',' + obj.colors[c][3] + ',1)';
+      grad.addColorStop(v, color);
+    }
+  }
+  ctx.fillStyle = grad;
+  ctx.fillRect(pos[0], pos[1], size[0], size[1]);
+  // Draw Range Labels
+  range[0] = Math.round(range[0]*1000)/1000;
+  range[1] = Math.round(range[1]*1000)/1000;
+  ctx.fillStyle = 'white';
+  ctx.font = textSizeY + 'px sans-serif';
+  ctx.txtBaseline = 'ideographic';
+  if (obj.orientation == 1){
+    ctx.fillText(range[1], pos[0], pos[1]-5);
+    ctx.fillText(range[0], pos[0], pos[1]+size[1]+textSizeY);
+  } else {
+    ctx.fillText(range[0], pos[0], pos[1]+size[1]+textSizeY);
+    txt = range[1].toString();
+    ctx.fillText(range[1], pos[0]+size[0]-((txt.length-1)*textSizeX), pos[1]+size[1]+textSizeY);
+  }
+  // Draw Title
+  ctx.fillStyle = 'white';
+  ctx.font = textSizeY + 'px sans-serif';
+  ctx.txtBaseline = 'ideographic';
+  if (obj.orientation == 1) ctx.fillText(obj.title, pos[0]+(obj.size[0]*render.view.width)/2-(obj.title.length*textSizeX/2), pos[1]-textSizeY-5);
+  else ctx.fillText(obj.title, pos[0]+size[0]/2-(obj.title.length*textSizeX/2), pos[1]-textSizeY-5);
+  // Draw Intervals' line
+  //Draw Interval make the render process slow
+  /*
+  interval = obj.numOfLabels-1;
+  if (obj.orientation == 1){
+    diff = size[1]/(interval-1);
+    y = pos[1]+size[1];
+    x = size[0]/2;
+    for(ii=0; ii<interval; ii++){
+      y = Math.floor(y) + 0.5;
+      if (ii%5) ctx.moveTo(pos[0]+2*x, y);
+      else ctx.moveTo(pos[0]+x, y);
+      ctx.lineTo(pos[0]+x*3, y);
+      ctx.lineWidth = 1;
+      ctx.strokeStyle = "white";
+      ctx.stroke();
+      y -= diff;
+    }
+  } else {
+    diff = size[0]/(interval-1);
+    y = size[1]/2;
+    x = pos[0];
+    for(ii=0; ii<interval; ii++){
+      x = Math.floor(x) + 0.5;
+      if (ii%5) ctx.moveTo(x, pos[1]);
+      else ctx.moveTo(x, pos[1]+y);
+      ctx.lineTo(x, pos[1]-y);
+      ctx.lineWidth = 1;
+      ctx.strokeStyle = "white";
+      ctx.stroke();
+      x += diff;
+    }
+  }/**/
+}
+
+WebGLRenderer.prototype.initBackground = function(c1, c2){
+  if (typeof(this.gl) == "undefined") return;
+  if (typeof(this.sceneJSON) == "undefined") return;
+
+  this.background = new Object();
+  this.background.vertices = new Float32Array([-1.0, -1.0, 0.0, 1.0, -1.0, 0.0, 1.0, 1.0, 0.0, -1.0, 1.0, 0.0]);
+  this.background.colors = new Float32Array([c1[0], c1[1], c1[2], 1.0,
+                                             c1[0], c1[1], c1[2], 1.0,
+                                             c2[0], c2[1], c2[2], 1.0,
+                                             c2[0], c2[1], c2[2], 1.0]);
+  this.background.index = new Uint16Array([0, 1, 2, 0, 2, 3]);
+  this.background.normals = new Float32Array([0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0]);
+
+  this.background.numberOfIndex = 6;
+
+  //Create Buffers
+  this.background.vbuff = this.gl.createBuffer(); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.background.vbuff);
+  this.gl.bufferData(this.gl.ARRAY_BUFFER, this.background.vertices, this.gl.STATIC_DRAW); this.background.vbuff.itemSize = 3;
+  this.background.nbuff = this.gl.createBuffer(); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.background.nbuff);
+  this.gl.bufferData(this.gl.ARRAY_BUFFER, this.background.normals, this.gl.STATIC_DRAW);  this.background.nbuff.itemSize = 3;
+  this.background.cbuff = this.gl.createBuffer(); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.background.cbuff);
+  this.gl.bufferData(this.gl.ARRAY_BUFFER, this.background.colors, this.gl.STATIC_DRAW);   this.background.cbuff.itemSize = 4;
+  this.background.ibuff = this.gl.createBuffer(); this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.background.ibuff);
+  this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, this.background.index, this.gl.STREAM_DRAW);
+}
+
+WebGLRenderer.prototype.renderBackground = function(){
+  if (this.background == null) return;
+
+  this.gl.useProgram(this.shaderProgram);
+  this.gl.uniform1i(this.shaderProgram.uIsLine, false);
+
+  mat4.translate(this.mvMatrix, [0.0, 0.0, -1.0]);
+  this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.background.vbuff);
+  this.gl.vertexAttribPointer(this.shaderProgram.vertexPositionAttribute, this.background.vbuff.itemSize, this.gl.FLOAT, false, 0, 0);
+  this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.background.nbuff);
+  this.gl.vertexAttribPointer(this.shaderProgram.vertexNormalAttribute, this.background.nbuff.itemSize, this.gl.FLOAT, false, 0, 0);
+  this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.background.cbuff);
+  this.gl.vertexAttribPointer(this.shaderProgram.vertexColorAttribute, this.background.cbuff.itemSize, this.gl.FLOAT, false, 0, 0);
+  this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.background.ibuff);
+  this.setMatrixUniforms(this.shaderProgram);
+  this.gl.drawElements(this.gl.TRIANGLES, this.background.numberOfIndex, this.gl.UNSIGNED_SHORT, 0);
+}
+
+WebGLRenderer.prototype.renderMesh = function(){
+  obj = this;
+  render = this.father;
+  render.gl.useProgram(render.shaderProgram);
+
+  render.gl.uniform1i(render.shaderProgram.uIsLine, false);
+
+  cameraRot = mat4.toRotationMat(render.mvMatrix);
+  mat4.transpose(cameraRot);
+  inverse = mat4.create(); mat4.inverse(cameraRot, inverse);
+  test = mat4.create(obj.matrix);
+  mat4.transpose(test);
+
+  icenter = [-render.sceneJSON.Center[0], -render.sceneJSON.Center[1], -render.sceneJSON.Center[2]];
+
+  mvPushMatrix(render.mvMatrix);
+  mat4.multiply(render.mvMatrix, cameraRot, render.mvMatrix);
+  if(obj.layer == 0) mat4.translate(render.mvMatrix, render.translation);
+  mat4.multiply(render.mvMatrix, inverse, render.mvMatrix);
+
+  if(obj.layer == 0) mat4.translate(render.mvMatrix, render.sceneJSON.Center);
+  mat4.multiply(render.mvMatrix, cameraRot, render.mvMatrix);
+  if(obj.layer == 0) mat4.scale(render.mvMatrix, [render.objScale, render.objScale, render.objScale], render.mvMatrix);
+  mat4.multiply(render.mvMatrix, render.rotMatrix, render.mvMatrix);
+  mat4.multiply(render.mvMatrix, inverse, render.mvMatrix);
+  if(obj.layer == 0) mat4.translate(render.mvMatrix, icenter);
+
+  render.rotMatrix2 = render.mvMatrix;
+
+  mat4.multiply(render.mvMatrix, test, render.mvMatrix);
+
+  render.gl.bindBuffer(render.gl.ARRAY_BUFFER, obj.vbuff);
+  render.gl.vertexAttribPointer(render.shaderProgram.vertexPositionAttribute, obj.vbuff.itemSize, render.gl.FLOAT, false, 0, 0);
+  render.gl.bindBuffer(render.gl.ARRAY_BUFFER, obj.nbuff);
+  render.gl.vertexAttribPointer(render.shaderProgram.vertexNormalAttribute, obj.nbuff.itemSize, render.gl.FLOAT, false, 0, 0);
+  render.gl.bindBuffer(render.gl.ARRAY_BUFFER, obj.cbuff);
+  render.gl.vertexAttribPointer(render.shaderProgram.vertexColorAttribute, obj.cbuff.itemSize, render.gl.FLOAT, false, 0, 0);
+  render.gl.bindBuffer(render.gl.ELEMENT_ARRAY_BUFFER, obj.ibuff);
+  render.setMatrixUniforms(render.shaderProgram);
+  render.gl.drawElements(render.gl.TRIANGLES, obj.numberOfIndex, render.gl.UNSIGNED_SHORT, 0);
+  render.mvMatrix = mvPopMatrix();
+}
+
+WebGLRenderer.prototype.renderLine = function(){
+  obj = this;
+  render = this.father;
+  render.gl.useProgram(render.shaderProgram);
+
+  render.gl.enable(render.gl.POLYGON_OFFSET_FILL);  //Avoid zfighting
+  render.gl.polygonOffset(-1.0, -1.0);
+
+  render.gl.uniform1i(render.shaderProgram.uIsLine, true);
+
+  cameraRot = mat4.toRotationMat(render.mvMatrix);
+  mat4.transpose(cameraRot);
+  inverse = mat4.create(); mat4.inverse(cameraRot, inverse);
+  test = mat4.create(obj.matrix);
+  mat4.transpose(test);
+
+  icenter = [-render.sceneJSON.Center[0], -render.sceneJSON.Center[1], -render.sceneJSON.Center[2]];
+
+  mvPushMatrix(render.mvMatrix);
+  mat4.multiply(render.mvMatrix, cameraRot, render.mvMatrix);
+  if(obj.layer == 0) mat4.translate(render.mvMatrix, render.translation);
+  mat4.multiply(render.mvMatrix, inverse, render.mvMatrix);
+
+  if(obj.layer == 0) mat4.translate(render.mvMatrix, render.sceneJSON.Center);
+  mat4.multiply(render.mvMatrix, cameraRot, render.mvMatrix);
+  if(obj.layer == 0) mat4.scale(render.mvMatrix, [render.objScale, render.objScale, render.objScale], render.mvMatrix);
+  mat4.multiply(render.mvMatrix, render.rotMatrix, render.mvMatrix);
+  mat4.multiply(render.mvMatrix, inverse, render.mvMatrix);
+  if(obj.layer == 0) mat4.translate(render.mvMatrix, icenter);
+
+  render.rotMatrix2 = render.mvMatrix;
+
+  mat4.multiply(render.mvMatrix, test, render.mvMatrix);
+
+  render.gl.bindBuffer(render.gl.ARRAY_BUFFER, obj.lbuff);
+  render.gl.vertexAttribPointer(render.shaderProgram.vertexPositionAttribute, obj.lbuff.itemSize, render.gl.FLOAT, false, 0, 0);
+  render.gl.bindBuffer(render.gl.ARRAY_BUFFER, obj.nbuff);
+  render.gl.vertexAttribPointer(render.shaderProgram.vertexNormalAttribute, obj.nbuff.itemSize, render.gl.FLOAT, false, 0, 0);
+  render.gl.bindBuffer(render.gl.ARRAY_BUFFER, obj.cbuff);
+  render.gl.vertexAttribPointer(render.shaderProgram.vertexColorAttribute, obj.cbuff.itemSize, render.gl.FLOAT, false, 0, 0);
+  render.gl.bindBuffer(render.gl.ELEMENT_ARRAY_BUFFER, obj.ibuff);
+  render.setMatrixUniforms(render.shaderProgram);
+  render.gl.drawElements(render.gl.LINES, obj.numberOfIndex, render.gl.UNSIGNED_SHORT, 0);
+  render.mvMatrix = mvPopMatrix();
+
+  render.gl.disable(render.gl.POLYGON_OFFSET_FILL);
+}
+
+WebGLRenderer.prototype.renderPoints = function(){
+  obj = this;
+  render = this.father;
+  render.gl.useProgram(render.pointShaderProgram);
+
+  render.gl.enable(render.gl.POLYGON_OFFSET_FILL);  //Avoid zfighting
+  render.gl.polygonOffset(-1.0, -1.0);
+
+  render.gl.uniform1f(render.pointShaderProgram.uPointSize, 2.0);//Wendel
+
+  cameraRot = mat4.toRotationMat(render.mvMatrix);
+  mat4.transpose(cameraRot);
+  inverse = mat4.create(); mat4.inverse(cameraRot, inverse);
+  test = mat4.create(obj.matrix);
+  mat4.transpose(test);
+
+  icenter = [-render.sceneJSON.Center[0], -render.sceneJSON.Center[1], -render.sceneJSON.Center[2]];
+
+  mvPushMatrix(render.mvMatrix);
+  mat4.multiply(render.mvMatrix, cameraRot, render.mvMatrix);
+  if(obj.layer == 0) mat4.translate(render.mvMatrix, render.translation);
+  mat4.multiply(render.mvMatrix, inverse, render.mvMatrix);
+
+  if(obj.layer == 0) mat4.translate(render.mvMatrix, render.sceneJSON.Center);
+  mat4.multiply(render.mvMatrix, cameraRot, render.mvMatrix);
+  if(obj.layer == 0) mat4.scale(render.mvMatrix, [render.objScale, render.objScale, render.objScale], render.mvMatrix);
+  mat4.multiply(render.mvMatrix, render.rotMatrix, render.mvMatrix);
+  mat4.multiply(render.mvMatrix, inverse, render.mvMatrix);
+  if(obj.layer == 0) mat4.translate(render.mvMatrix, icenter);
+
+  render.rotMatrix2 = render.mvMatrix;
+
+  mat4.multiply(render.mvMatrix, test, render.mvMatrix);
+
+  render.gl.bindBuffer(render.gl.ARRAY_BUFFER, obj.pbuff);
+  render.gl.vertexAttribPointer(render.pointShaderProgram.vertexPositionAttribute, obj.pbuff.itemSize, render.gl.FLOAT, false, 0, 0);
+  render.gl.bindBuffer(render.gl.ARRAY_BUFFER, obj.cbuff);
+  render.gl.vertexAttribPointer(render.pointShaderProgram.vertexColorAttribute, obj.cbuff.itemSize, render.gl.FLOAT, false, 0, 0);
+  render.setMatrixUniforms(render.pointShaderProgram);
+  render.gl.drawArrays(render.gl.POINTS, 0, obj.numberOfPoints);//Wendel
+  render.mvMatrix = mvPopMatrix();
+
+  render.gl.disable(render.gl.POLYGON_OFFSET_FILL);
+}
+
+WebGLRenderer.prototype.setMatrixUniforms = function(s) {
+  mvMatrixInv = mat4.create();
+  normal = mat4.create();
+  mat4.inverse(this.mvMatrix, mvMatrixInv);
+  mat4.transpose(mvMatrixInv, normal);
+
+  this.gl.uniformMatrix4fv(s.pMatrixUniform, false, this.pMatrix);
+  this.gl.uniformMatrix4fv(s.mvMatrixUniform, false, this.mvMatrix);
+  if(s.nMatrixUniform != null) this.gl.uniformMatrix4fv(s.nMatrixUniform, false, normal);
+}
+
+WebGLRenderer.prototype.processObject = function() {
+  if (this.processQueue.length != 0){
+    obj = this.processQueue[this.processQueue.length-1];
+    this.processQueue.length -= 1;
+    this.parseObject(obj);
+  }
+}
+
+WebGLRenderer.prototype.drawScene = function() {
+  this.drawInterval = requestAnimFrame(new Function("webglRenderers['" + this.view.id + "'].drawScene();"));
+  if (this.hasSceneChanged){
+    this.updateScene();
+  }
+  if (this.sceneJSON == null){
+    return;
+  }
+  this.frames++;
+
+  if(this.frames >= 50 && this.nbErrors < 5){
+    this.frames = 0;
+    ko = new Date();
+    currTime = ko.getTime();
+    diff = currTime - this.lastTime;
+    this.lastTime = currTime;
+    this.fps = 50000/diff;
+  }
+  this.processObject();
+
+  this.gl.viewport(0, 0, this.gl.viewportWidth, this.gl.viewportHeight);
+  this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
+
+  mat4.ortho(-1.0, 1.0, -1.0, 1.0, 1.0, 1000000.0, this.pMatrix);
+  mat4.identity(this.mvMatrix);
+  this.gl.disable(this.gl.DEPTH_TEST);
+  this.renderBackground();
+  this.gl.enable(this.gl.DEPTH_TEST);
+
+  this.ctx2d.clearRect(0, 0, this.view.width, this.view.height);
+  for(rr=this.sceneJSON.Renderers.length-1; rr>=0 ; rr--){
+    renderer = this.sceneJSON.Renderers[rr];
+    width  = renderer.size[0]-renderer.origin[0];
+    height = renderer.size[1]-renderer.origin[1];
+    width = width*this.view.width;
+    height = height*this.view.height;
+    x = renderer.origin[0]*this.view.width;
+    y = renderer.origin[1]*this.view.height;
+    if (y < 0) y = 0;
+    this.gl.viewport(x, y, width, height);
+    //this.gl.clear(this.gl.DEPTH_BUFFER_BIT);
+    mat4.perspective(renderer.LookAt[0], width/height, 0.1, 1000000.0, this.pMatrix);
+    mat4.identity(this.mvMatrix);
+    mat4.lookAt([renderer.LookAt[7], renderer.LookAt[8], renderer.LookAt[9]],
+                [renderer.LookAt[1], renderer.LookAt[2], renderer.LookAt[3]],
+                [renderer.LookAt[4], renderer.LookAt[5], renderer.LookAt[6]],
+                this.mvMatrix);
+
+    for(r=0; r<this.objects.length; r++){
+      if (!this.objects[r].hasTransparency && this.objects[r].layer == rr) this.objects[r].render();
+    }
+    //Render Objects with Transparency
+    this.gl.enable(this.gl.BLEND);                //Enable transparency
+    this.gl.enable(this.gl.POLYGON_OFFSET_FILL);  //Avoid zfighting
+    this.gl.polygonOffset(-1.0, -1.0);
+    for(r=0; r<this.objects.length; r++){
+      if (this.objects[r].hasTransparency && this.objects[r].layer == rr) this.objects[r].render();
+    }
+    this.gl.disable(this.gl.POLYGON_OFFSET_FILL);
+    this.gl.disable(this.gl.BLEND);
+  }
+}
+
+WebGLRenderer.prototype.initShaders = function() {
+  this.shaderProgram;
+  this.pointShaderProgram;
+
+  var fragmentShader = this.getShader("shader-fs");
+  var vertexShader = this.getShader("shader-vs");
+  var pointFragShader = this.getShader("shader-fs-Point");
+  var pointVertShader = this.getShader("shader-vs-Point");
+
+  this.shaderProgram = this.gl.createProgram();
+  this.gl.attachShader(this.shaderProgram, vertexShader);
+  this.gl.attachShader(this.shaderProgram, fragmentShader);
+  this.gl.linkProgram(this.shaderProgram);
+  if (!this.gl.getProgramParameter(this.shaderProgram, this.gl.LINK_STATUS)) {
+      alert("Could not initialise shaders");
+  }
+  this.pointShaderProgram = this.gl.createProgram();
+  this.gl.attachShader(this.pointShaderProgram, pointVertShader);
+  this.gl.attachShader(this.pointShaderProgram, pointFragShader);
+  this.gl.linkProgram(this.pointShaderProgram);
+  if (!this.gl.getProgramParameter(this.pointShaderProgram, this.gl.LINK_STATUS)) {
+      alert("Could not initialise the point shaders");
+  }
+
+  this.gl.useProgram(this.pointShaderProgram);
+  this.pointShaderProgram.vertexPositionAttribute = this.gl.getAttribLocation(this.pointShaderProgram, "aVertexPosition");
+  this.gl.enableVertexAttribArray(this.pointShaderProgram.vertexPositionAttribute);
+  this.pointShaderProgram.vertexColorAttribute = this.gl.getAttribLocation(this.pointShaderProgram, "aVertexColor");
+  this.gl.enableVertexAttribArray(this.pointShaderProgram.vertexColorAttribute);
+  this.pointShaderProgram.pMatrixUniform = this.gl.getUniformLocation(this.pointShaderProgram, "uPMatrix");
+  this.pointShaderProgram.mvMatrixUniform = this.gl.getUniformLocation(this.pointShaderProgram, "uMVMatrix");
+  this.pointShaderProgram.nMatrixUniform = this.gl.getUniformLocation(this.pointShaderProgram, "uNMatrix");
+  this.pointShaderProgram.uPointSize = this.gl.getUniformLocation(this.pointShaderProgram, "uPointSize");
+
+  this.gl.useProgram(this.shaderProgram);
+  this.shaderProgram.vertexPositionAttribute = this.gl.getAttribLocation(this.shaderProgram, "aVertexPosition");
+  this.gl.enableVertexAttribArray(this.shaderProgram.vertexPositionAttribute);
+  this.shaderProgram.vertexColorAttribute = this.gl.getAttribLocation(this.shaderProgram, "aVertexColor");
+  this.gl.enableVertexAttribArray(this.shaderProgram.vertexColorAttribute);
+  this.shaderProgram.vertexNormalAttribute = this.gl.getAttribLocation(this.shaderProgram, "aVertexNormal");
+  this.gl.enableVertexAttribArray(this.shaderProgram.vertexNormalAttribute);
+  this.shaderProgram.pMatrixUniform = this.gl.getUniformLocation(this.shaderProgram, "uPMatrix");
+  this.shaderProgram.mvMatrixUniform = this.gl.getUniformLocation(this.shaderProgram, "uMVMatrix");
+  this.shaderProgram.nMatrixUniform = this.gl.getUniformLocation(this.shaderProgram, "uNMatrix");
+  this.shaderProgram.uIsLine = this.gl.getUniformLocation(this.shaderProgram, "uIsLine");
+}
+
+WebGLRenderer.prototype.getShader = function(id) {
+    var shaderScript = document.getElementById(id);
+    if (!shaderScript) {
+        return null;
+    }
+
+    var str = "";
+    var k = shaderScript.firstChild;
+    while (k) {
+        if (k.nodeType == 3) {
+            str += k.textContent;
+        }
+        k = k.nextSibling;
+    }
+
+    var shader;
+    if (shaderScript.type == "x-shader/x-fragment") {
+        shader = this.gl.createShader(this.gl.FRAGMENT_SHADER);
+    } else if (shaderScript.type == "x-shader/x-vertex") {
+        shader = this.gl.createShader(this.gl.VERTEX_SHADER);
+    } else {
+        return null;
+    }
+
+    this.gl.shaderSource(shader, str);
+    this.gl.compileShader(shader);
+
+    if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
+        alert(this.gl.getShaderInfoLog(shader));
+        return null;
+    }
+
+    return shader;
+}
+
+WebGLRenderer.prototype.forceUpdateCamera = function(){
+  if (typeof(this.gl) == "undefined" || this.gl == null || renderers.current != this) return;
+  if (this.offlineMode) return;
+  render = this;
+
+  pos = [render.lookAt[7], render.lookAt[8], render.lookAt[9]];
+  up  = [render.lookAt[4], render.lookAt[5], render.lookAt[6]];
+  fp  = [render.lookAt[1], render.lookAt[2], render.lookAt[3]];
+  tt  = [render.translation[0], render.translation[1], 0.0];
+  center = [render.sceneJSON.Center[0], render.sceneJSON.Center[1], render.sceneJSON.Center[2]];
+
+  cameraRot = mat4.toRotationMat(render.mvMatrix);
+  mat4.transpose(cameraRot);
+  inverse = mat4.create(); mat4.inverse(cameraRot, inverse);
+
+  inv = mat4.create();
+  mat4.identity(inv);
+  mat4.multiply(inv, cameraRot, inv);
+  mat4.scale(inv, [render.objScale, render.objScale, render.objScale], inv);
+  mat4.multiply(inv, render.rotMatrix, inv);
+  mat4.multiply(inv, inverse, inv);
+
+  mat4.inverse(inv, inv);
+  fp = vec3.subtract(fp, center, fp);
+  pos = vec3.subtract(pos, center, pos);
+  mat4.multiplyVec3(inv, fp, fp);
+  mat4.multiplyVec3(inv, pos, pos);
+  mat4.multiplyVec3(inv, up, up);
+  fp = vec3.add(fp, center, fp);
+  pos = vec3.add(pos, center, pos);
+  vec3.normalize(up, up);
+
+  tt2 = [0, 0, 0];
+  tt2[0] += tt[0]*render.right[0];
+  tt2[1] += tt[0]*render.right[1];
+  tt2[2] += tt[0]*render.right[2];
+  tt2[0] += tt[1]*render.up[0];
+  tt2[1] += tt[1]*render.up[1];
+  tt2[2] += tt[1]*render.up[2];
+
+  vec3.subtract(pos, tt2, pos);
+  vec3.subtract(fp , tt2, fp);
+
+  paraviewObjects[parseInt(render.sessionId)].sendEvent("UpdateCamera", render.viewId + " " + fp[0] + " " + fp[1] + " " + fp[2]
+  + " " + up[0] + " " + up[1] + " " + up[2] + " " + pos[0] + " " + pos[1] + " " + pos[2]);
+  clearTimeout(this.updateId);
+  this.updateId = setTimeout("webglRenderers[\'" + this.view.id + "\'].updateCamera()", this.updateInterval);
+}
+
+WebGLRenderer.prototype.updateCamera = function(){
+  if (!this.mouseDown){
+    this.updateId = setTimeout("webglRenderers[\'" + this.view.id + "\'].updateCamera()", this.updateInterval);
+    return;
+  }
+  if (this.serverMode) return;
+  this.forceUpdateCamera();
+}
+
+/**********************************************************************************/
+
+var mvMatrixStack = [];
+function mvPushMatrix(m) {
+    var copy = mat4.create();
+    mat4.set(m, copy);
+    mvMatrixStack.push(copy);
+}
+
+function mvPopMatrix() {
+  if (mvMatrixStack.length == 0) {
+    throw "Invalid popMatrix!";
+  }
+  return mvMatrixStack.pop();
+}
+
+
+function handleMouseDown(event, id) {
+  render = webglRenderers[id];
+  render.mouseDown = true;
+  render.lastMouseX = event.clientX;
+  render.lastMouseY = event.clientY;
+  if (!render.offlineMode){
+    paraviewObjects[render.sessionId].sendEvent('MouseEvent', render.viewId + ' 0 0');
+    updateRendererSize(render.sessionId, render.viewId, render.view.width/render.interactionRatio, render.view.height/render.interactionRatio);
+  }
+  event.preventDefault();
+  return false;
+}
+
+function handleMouseUp(event, id) {
+  render = webglRenderers[id];
+  render.mouseDown = false;
+  if (!render.offlineMode){
+    paraviewObjects[render.sessionId].sendEvent('MouseEvent', render.viewId + ' 2 0');
+    updateRendererSize(render.sessionId, render.viewId, render.view.width, render.view.height);
+    render.forceUpdateCamera();
+    render.requestMetaData();
+  }
+  event.preventDefault();
+}
+
+function handleMouseMove(event, id) {
+  render = webglRenderers[id];
+  if (!render.mouseDown) {
+    return;
+  }
+  var newX = event.clientX;
+  var newY = event.clientY;
+  var deltaX = newX - render.lastMouseX;
+  var deltaY = newY - render.lastMouseY;
+
+  if (event.button == 0){
+    var rX = deltaX/50.0;
+    var rY = deltaY/50.0;
+    var mx = mat4.create(); mat4.identity(mx); mat4.rotate(mx, rX, [0, 1, 0]);
+    var my = mat4.create(); mat4.identity(my); mat4.rotate(my, rY, [1, 0, 0]);
+    mat4.multiply(mx, my, mx);
+    mat4.multiply(mx, render.rotMatrix, render.rotMatrix);
+  } else if (event.button == 1){
+    z = Math.abs(render.sceneJSON.Renderers[0].LookAt[9]-render.sceneJSON.Renderers[0].LookAt[3]);
+    aux = z/render.objScale;
+    render.translation[0] += aux*deltaX/1500.0;
+    render.translation[1] -= aux*deltaY/1500.0;
+  } else if (event.button == 2){
+    render.objScale += render.objScale*(deltaY)/200.0;
+  } else {
+    render.objScale += render.objScale*(deltaY)/200.0;
+  }
+
+  render.lastMouseX = newX;
+  render.lastMouseY = newY;
+
+  event.preventDefault();
+}
+
+function mouseServerInt(rendererId, sessionId, viewId, action, event){
+    consumeEvent(event);
+    render = webglRenderers[rendererId];
+    render.interaction.lastRealEvent = event;
+    var width = render.view.width;
+    var height = render.view.height;
+
+    if(action == 'down') {
+        if(render.interaction.needUp) {
+            paraviewObjects[sessionId].sendEvent('MouseEvent', viewId + ' 2 ' + render.interaction.lastEvent);
+        }
+        render.interaction.isDragging = true;
+        render.interaction.needUp = true;
+        switch(event.button){
+            case 1:
+                render.interaction.button =  '0 ';
+                break;
+            case 4:
+                render.interaction.button =  '1 ';
+                break;
+            case 2:
+                render.interaction.button =  '2 ';
+                break;
+        }
+        render.interaction.action = " 0 ";
+        render.interaction.keys = "";
+        if(event.ctrlKey) {
+            render.interaction.keys += "1";
+        } else {
+            render.interaction.keys += "0";
+        }
+        if(event.shiftKey) {
+            render.interaction.keys += " 1";
+        } else {
+            render.interaction.keys += " 0";
+        }
+        render.interaction.x = event.screenX;
+        render.interaction.y = event.screenY;
+
+        // Keep relative origin
+        var docX = event.pageX;
+        var docY = event.pageY;
+        render.interaction.xOrigin = docX - render.getPageX();
+        render.interaction.yOrigin = docY - render.getPageY();
+    } else if (action == 'move') {
+        render.interaction.action = " 1 ";
+    } else if ( action == 'up' || action == 'click') {
+        render.interaction.isDragging = false;
+        render.interaction.needUp = false;
+        render.interaction.action = " 2 ";
+        //
+        var mouseInfo = ((event.screenX-render.interaction.x + render.interaction.xOrigin)/height) + " " + (1-(event.screenY-render.interaction.y + render.interaction.yOrigin)/height) + " " + render.interaction.keys ;
+        render.interaction.lastEvent = render.interaction.button + mouseInfo;
+        paraviewObjects[sessionId].sendEvent('MouseEvent', viewId + render.interaction.action + render.interaction.lastEvent);
+        render.interaction.scale = 1;
+        render.interaction.button = event.button + ' ';
+        render.interaction.keys = "0 0"
+    }
+    if(render.interaction.isDragging ){
+        var mouseInfoDrag = ((event.screenX-render.interaction.x + render.interaction.xOrigin)/height) + " " + (1-(event.screenY-render.interaction.y + render.interaction.yOrigin)/height) + " " + render.interaction.keys ;
+        var mouseAction = 'MouseEvent';
+        if(action == 'move') {
+            mouseAction = 'MouseMove';
+        }
+        render.interaction.lastEvent = render.interaction.button + mouseInfoDrag;
+        paraviewObjects[sessionId].sendEvent(mouseAction, viewId + render.interaction.action + render.interaction.lastEvent);
+    }
+    return false;
+}